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).
This commit is contained in:
Brad Arant committed 2026-09-06 04:08:57 +00:00
1 parent cd5e0a9905
commit e8b2a79907
17 files changed
+667 -12

No files matched your search

+63 -9
View File
@@ -37,6 +37,29 @@ namespace mail {
out << "252 " << errorString << CRLF;
}
void SMTPServer::deliverToMailbox(coreutils::ZString &mailbox, coreutils::ZString &messageId,
SpamAssassinFilter::Result &scan) {
// The destination folder has to be picked AFTER the scan runs,
// not before - this used to always build an "/Inbox/" path
// regardless of what spamd found, so a correctly-flagged spam
// message (isSpam true, logged as "(flagged as spam)" right
// below) still landed straight in Inbox every time; the Spam
// folder existed and was reachable, it just never received
// anything. scan.isSpam is only meaningful when scan.ok is
// true (spamd fails open - see SpamAssassinFilter.h's own doc
// comment - an unreachable scanner must not silently reroute
// every message to Spam).
coreutils::MString path;
path << mailFileSystem.getMailBoxPath(mailbox)
<< ((scan.ok && scan.isSpam) ? "/Spam/" : "/Inbox/") << messageId;
coreutils::File mailFile(path, O_CREAT | O_WRONLY, 0660);
mailFile.write(scan.scanned);
coreutils::Log(coreutils::LOG_INFO) << "Message " << messageId << " delivered to recipient " << mailbox
<< (scan.ok && scan.isSpam ? " (flagged as spam)" : "") << ".";
}
void SMTPServer::inCreate(coreutils::ZString &name) {
coreutils::Log(coreutils::LOG_DEBUG_2) << "inCreate; [" << name << "]";
name.split("|");
@@ -45,11 +68,15 @@ namespace mail {
// had to be added alongside the recipient.
if(name.getList().size() > 2) {
bool exists = mailFileSystem.ifMailBoxExists(name[1]);
coreutils::MString path;
path << mailFileSystem.getMailBoxPath(name[1]) << "/Inbox/" << name[0];
// An alias is a plain file (not a directory) at the same
// level a mailbox would be - see MailFileSystem's own doc
// comment on createAlias()/ifAliasExists(). Only checked when
// it isn't a real mailbox, so a mailbox and an alias can never
// both match the same address.
bool isAlias = !exists && mailFileSystem.ifAliasExists(name[1]);
coreutils::MString from;
from << mailFileSystem.getQueuePath() << "/" << name;
if(exists) {
if(exists || isAlias) {
// Scanned on the way into the recipient's own Inbox, not the
// outbound relay path below - spamd's added X-Spam-* headers
// are for whatever reads this recipient's mailbox, and a
@@ -62,12 +89,18 @@ namespace mail {
queuedFile.read();
SpamAssassinFilter::Result scan = SpamAssassinFilter::scan(queuedFile.asString());
coreutils::File mailFile(path, O_CREAT | O_WRONLY, 0660);
mailFile.write(scan.scanned);
int rc = unlink(from.c_str());
if (isAlias) {
// One shared scan result delivered to every target - an
// alias should carry one spam verdict for the message, not
// a separately re-scanned copy per target.
std::vector<coreutils::MString> targets = mailFileSystem.getAliasMailboxes(name[1]);
for (auto &target : targets)
deliverToMailbox(target, name[0], scan);
} else {
deliverToMailbox(name[1], name[0], scan);
}
coreutils::Log(coreutils::LOG_INFO) << "Message " << name[0] << " delivered to recipient " << name[1]
<< (scan.ok && scan.isSpam ? " (flagged as spam)" : "") << ".";
int rc = unlink(from.c_str());
} else {
// Not a local mailbox - relay it out over SMTP. SendMail's
// own send() is a plain blocking call (see its doc comment
@@ -82,8 +115,29 @@ namespace mail {
coreutils::MString mailData(queuedFile.asString());
SendMail *outbound = new SendMail(hostName, mailFileSystem.getSendLogPath().str(),
name[0].str(), sender, recipient, mailData, dkimSigner);
// The queue file's content is already fully read into
// mailData above, so it's safe to unlink it now rather
// than after the detached thread finishes (which may be
// arbitrarily later, or never, if the process restarts
// first) - previously this was never unlinked at all,
// meaning processExisting() re-relayed every message still
// sitting here on every single restart, sending duplicate
// copies (confirmed live: one message redelivered 35+
// times across repeated crash-restarts).
int rc = unlink(from.c_str());
std::thread([outbound]() {
outbound->send();
try {
outbound->send();
} catch (...) {
// An exception escaping a detached thread's entry
// function calls std::terminate() - crashing the
// entire process, not just this one relay attempt
// (this, combined with SIGPIPE previously not being
// ignored anywhere, is what turned an occasional
// failed delivery into repeated whole-server crashes
// - see main.cpp's new signal(SIGPIPE, SIG_IGN)).
coreutils::Log(coreutils::LOG_EXCEPT) << "Outbound relay threw an exception - message not delivered.";
}
delete outbound;
}).detach();
}