Files
BARANTMail/SMTPServer.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

156 lines
7.3 KiB
C++

#include "SMTPServer.h"
#include "Log.h"
#include "SendMail.h"
#include "SpamAssassinFilter.h"
#include "File.h"
#include <thread>
namespace mail {
SMTPServer::SMTPServer(core::EPoll &ePoll, std::string hostName, MailFileSystem &mailFileSystem,
core::IPAddress ipAddress, core::TLSInfo *tlsInfo, bool watchQueue,
DKIMSigner *dkimSigner)
: core::TCPServer(ePoll, ipAddress, tlsInfo, " ", 1, "SMTP Server"), core::INotify(ePoll), ePoll(ePoll), hostName(hostName), mailFileSystem(mailFileSystem), dkimSigner(dkimSigner) {
commands.add(_smtp_auth, "AUTH");
commands.add(_smtp_data, "DATA");
commands.add(_smtp_ehlo, "EHLO");
commands.add(_smtp_expn, "EXPN");
commands.add(_smtp_helo, "HELO");
commands.add(_smtp_help, "HELP");
commands.add(_smtp_mail, "MAIL");
commands.add(_smtp_noop, "NOOP");
commands.add(_smtp_quit, "QUIT");
commands.add(_smtp_rcpt, "RCPT");
commands.add(_smtp_rset, "RSET");
commands.add(_smtp_vrfy, "VRFY");
if (watchQueue) {
processExisting();
wd = addWatch(mailFileSystem.getQueuePath());
}
}
SMTPSession * SMTPServer::getSocketAccept(core::EPoll &ePoll) {
return new SMTPSession(ePoll, *this);
}
void SMTPServer::sessionErrorHandler(std::string errorString, std::stringstream &out) {
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("|");
// Three parts now: messageid|recipient|sender - see
// __SMTP_DATA::queueMail()'s own doc comment for why the sender
// had to be added alongside the recipient.
if(name.getList().size() > 2) {
bool exists = mailFileSystem.ifMailBoxExists(name[1]);
// 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 || 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
// message this server relays onward gets scored by ITS
// destination's own filtering, same as any other MTA. Reading
// the whole file in (rather than the plain link() this branch
// used to do) is unavoidable now that spamd may have rewritten
// the content the recipient actually needs to receive.
coreutils::File queuedFile(from, O_RDONLY);
queuedFile.read();
SpamAssassinFilter::Result scan = SpamAssassinFilter::scan(queuedFile.asString());
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);
}
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
// for why) - queued delivery wants fire-and-forget, so this
// owns the heap-allocated SendMail itself and runs it on its
// own detached thread rather than blocking this EPoll
// worker thread for however long the remote host takes.
coreutils::MString sender(name[2]);
coreutils::MString recipient(name[1]);
coreutils::File queuedFile(from, O_RDONLY);
queuedFile.read();
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]() {
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();
}
}
}
void SMTPServer::processExisting() {
for (const auto & entry : std::filesystem::directory_iterator(mailFileSystem.getQueuePath().str())) {
std::string temp(entry.path().filename().string());
coreutils::ZString temp2(temp);
inCreate(temp2);
}
}
}