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
+666 -11

No files matched your search

+2
View File
@@ -5,3 +5,5 @@ Release
*~
*.mk
libBMAMail.a
barantmail.cfg
*.log

Before

Width:  |  Height:  |  Size: 47 B

After

Width:  |  Height:  |  Size: 68 B

+22
View File
@@ -14,6 +14,12 @@ namespace mail {
if (!file->exists("smtp.port"))
throw coreutils::Exception("BARANTMailConfig: \"smtp.port\" is missing from " + path);
if ((file->exists("admin.user") || file->exists("admin.password") || file->exists("admin.port")) &&
(!file->exists("admin.user") || !file->exists("admin.password")))
throw coreutils::Exception(
"BARANTMailConfig: \"admin\" is configured but \"admin.user\"/\"admin.password\" "
"is missing from " + path);
bool needsTls = file->exists("smtpTLS.port") || file->exists("imap.port") || file->exists("pop3.port");
if (needsTls) {
if (!file->exists("tls.certificate") || !file->exists("tls.key"))
@@ -92,4 +98,20 @@ namespace mail {
return file->getString("dkim.privateKey");
}
bool BARANTMailConfig::hasAdmin() {
return file->exists("admin.user");
}
int BARANTMailConfig::getAdminPort() {
return file->exists("admin.port") ? (int)file->getInt("admin.port") : 1027;
}
std::string BARANTMailConfig::getAdminUser() {
return file->getString("admin.user");
}
std::string BARANTMailConfig::getAdminPassword() {
return file->getString("admin.password");
}
}
+13
View File
@@ -83,6 +83,19 @@ namespace mail {
std::string getDkimSelector();
std::string getDkimPrivateKeyPath();
///
/// The admin console (see ConsoleServer/ConsoleSession) - "port"
/// defaults to 1027 if omitted. "user"/"password" are required
/// once "admin" is present at all; there is no path through this
/// class that hands back an admin listener with nothing to check
/// logins against.
///
bool hasAdmin();
int getAdminPort();
std::string getAdminUser();
std::string getAdminPassword();
private:
coreutils::JFile *file;
BIN
View File
Binary file not shown.
+211
View File
@@ -4,6 +4,7 @@
# include "Log.h"
# include "File.h"
# include <filesystem>
# include <cerrno>
namespace mail {
@@ -51,7 +52,27 @@ namespace mail {
}
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;
}
@@ -113,4 +134,194 @@ namespace mail {
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;
}
}
+27
View File
@@ -40,6 +40,33 @@ namespace mail {
coreutils::MString getFolderPath(coreutils::ZString &mailbox, std::string folder);
bool ifFolderExists(coreutils::ZString &mailbox, std::string folder);
// Domain/mailbox/alias maintenance for the admin console (see
// __ADMIN_DOMAIN/__ADMIN_MAILBOX/__ADMIN_ALIAS) - a domain is
// just a directory directly under mailPath; a mailbox is a
// directory under that with Inbox/Spam/Trash and a .password
// file (matching checkPassword()'s own expectations exactly);
// an alias is a plain FILE at that same level (matching
// ifAliasExists()'s own S_ISREG check) listing its target
// mailboxes one per line. Each returns false and fills `error`
// with a human-readable reason on failure, rather than throwing -
// the console commands calling these want to report a clean
// failure line to the operator, not crash the whole server on a
// typo'd domain name.
bool createDomain(coreutils::ZString &domain, std::string &error);
bool removeDomain(coreutils::ZString &domain, std::string &error);
std::vector<coreutils::MString> listDomains();
bool createMailbox(coreutils::ZString &mailbox, coreutils::ZString &password, std::string &error);
bool removeMailbox(coreutils::ZString &mailbox, std::string &error);
std::vector<coreutils::MString> listMailboxes(coreutils::ZString &domain);
bool createAlias(coreutils::ZString &alias, coreutils::ZString &targets, std::string &error);
bool removeAlias(coreutils::ZString &alias, std::string &error);
std::vector<coreutils::MString> listAliases(coreutils::ZString &domain);
bool setPassword(coreutils::ZString &mailbox, coreutils::ZString &newPassword, std::string &error);
private:
coreutils::ZString &mailPath;
coreutils::MString queuePath;
+62 -8
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]() {
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();
}
+9
View File
@@ -24,6 +24,7 @@
# include "INotify.h"
# include "SendMail.h"
# include "DKIMSigner.h"
# include "SpamAssassinFilter.h"
namespace mail {
class SMTPServer : public core::TCPServer, public core::INotify {
@@ -78,6 +79,14 @@ namespace mail {
void processExisting();
// Writes one already-scanned message into one mailbox's Inbox
// or Spam folder - factored out of inCreate() so an alias
// (MailFileSystem::ifAliasExists()/getAliasMailboxes()) can
// deliver the same scanned copy to each of its target mailboxes,
// not just the single recipient a plain local delivery has.
void deliverToMailbox(coreutils::ZString &mailbox, coreutils::ZString &messageId,
SpamAssassinFilter::Result &scan);
};
}
+59
View File
@@ -0,0 +1,59 @@
#include "__ADMIN_ALIAS.h"
namespace mail {
__ADMIN_ALIAS::__ADMIN_ALIAS(MailFileSystem &mailFileSystem) : mailFileSystem(mailFileSystem) {}
int __ADMIN_ALIAS::processCommand(coreutils::ZString &request, core::TCPSession &session) {
if (request.getList().size() < 2) {
session.out << "Usage: alias add alias@domain target@domain[,target2@domain] | remove alias@domain | list domain" << "\r\n";
return 1;
}
std::string verb = request[1].str();
std::string error;
if (verb == "add") {
if (request.getList().size() < 4) {
session.out << "Usage: alias add alias@domain target@domain[,target2@domain]" << "\r\n";
return 1;
}
coreutils::ZString alias(request[2]);
coreutils::ZString targets(request[3]);
bool ok = mailFileSystem.createAlias(alias, targets, error);
session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n";
} else if (verb == "remove") {
if (request.getList().size() < 3) {
session.out << "Usage: alias remove alias@domain" << "\r\n";
return 1;
}
coreutils::ZString alias(request[2]);
bool ok = mailFileSystem.removeAlias(alias, error);
session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n";
} else if (verb == "list") {
if (request.getList().size() < 3) {
session.out << "Usage: alias list domain" << "\r\n";
return 1;
}
coreutils::ZString domain(request[2]);
for (auto &alias : mailFileSystem.listAliases(domain)) {
// listAliases() only returns the local part (the filename
// on disk) - getAliasMailboxes() needs the full "user@domain"
// form since it calls getMailBoxPath() itself to find that
// same file.
coreutils::MString fullAddress;
fullAddress << alias << "@" << domain;
std::string fullAddressStr = fullAddress.str();
coreutils::ZString aliasAddress(fullAddressStr);
session.out << alias << "@" << domain << " -> ";
auto targets = mailFileSystem.getAliasMailboxes(aliasAddress);
for (size_t i = 0; i < targets.size(); ++i)
session.out << (i ? ", " : "") << targets[i];
session.out << "\r\n";
}
} else {
session.out << "Usage: alias add alias@domain target@domain[,target2@domain] | remove alias@domain | list domain" << "\r\n";
}
return 1;
}
}
+27
View File
@@ -0,0 +1,27 @@
#ifndef ____ADMIN_ALIAS_h__
# define ____ADMIN_ALIAS_h__
# include "Command.h"
# include "MailFileSystem.h"
namespace mail {
///
/// Admin console command: "alias add alias@domain target1@domain,target2@domain",
/// "alias remove alias@domain", "alias list domain" - see
/// MailFileSystem::createAlias()/removeAlias()/listAliases(). This is
/// also what makes aliases actually deliver mail - see
/// SMTPServer::inCreate()'s own use of ifAliasExists()/getAliasMailboxes().
///
class __ADMIN_ALIAS : public core::Command {
public:
__ADMIN_ALIAS(MailFileSystem &mailFileSystem);
int processCommand(coreutils::ZString &request, core::TCPSession &session) override;
private:
MailFileSystem &mailFileSystem;
};
}
#endif
+37
View File
@@ -0,0 +1,37 @@
#include "__ADMIN_DOMAIN.h"
namespace mail {
__ADMIN_DOMAIN::__ADMIN_DOMAIN(MailFileSystem &mailFileSystem) : mailFileSystem(mailFileSystem) {}
int __ADMIN_DOMAIN::processCommand(coreutils::ZString &request, core::TCPSession &session) {
// request was already split (by the console's own top-level
// CommandList, delimiter " ") before dispatch reached here, so
// request[0] is "domain" itself, request[1] the verb, request[2]
// the domain name (list has no third argument).
if (request.getList().size() < 2) {
session.out << "Usage: domain add|remove|list [name]" << "\r\n";
return 1;
}
std::string verb = request[1].str();
std::string error;
if (verb == "add" || verb == "remove") {
if (request.getList().size() < 3) {
session.out << "Usage: domain " << verb << " <name>" << "\r\n";
return 1;
}
coreutils::ZString domain(request[2]);
bool ok = (verb == "add") ? mailFileSystem.createDomain(domain, error)
: mailFileSystem.removeDomain(domain, error);
session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n";
} else if (verb == "list") {
for (auto &domain : mailFileSystem.listDomains())
session.out << domain << "\r\n";
} else {
session.out << "Usage: domain add|remove|list [name]" << "\r\n";
}
return 1;
}
}
+25
View File
@@ -0,0 +1,25 @@
#ifndef ____ADMIN_DOMAIN_h__
# define ____ADMIN_DOMAIN_h__
# include "Command.h"
# include "MailFileSystem.h"
namespace mail {
///
/// Admin console command: "domain add|remove|list [name]" - see
/// MailFileSystem::createDomain()/removeDomain()/listDomains(),
/// which this just parses arguments for and reports the result of.
///
class __ADMIN_DOMAIN : public core::Command {
public:
__ADMIN_DOMAIN(MailFileSystem &mailFileSystem);
int processCommand(coreutils::ZString &request, core::TCPSession &session) override;
private:
MailFileSystem &mailFileSystem;
};
}
#endif
+46
View File
@@ -0,0 +1,46 @@
#include "__ADMIN_MAILBOX.h"
namespace mail {
__ADMIN_MAILBOX::__ADMIN_MAILBOX(MailFileSystem &mailFileSystem) : mailFileSystem(mailFileSystem) {}
int __ADMIN_MAILBOX::processCommand(coreutils::ZString &request, core::TCPSession &session) {
if (request.getList().size() < 2) {
session.out << "Usage: mailbox add user@domain password | remove user@domain | list domain" << "\r\n";
return 1;
}
std::string verb = request[1].str();
std::string error;
if (verb == "add") {
if (request.getList().size() < 4) {
session.out << "Usage: mailbox add user@domain password" << "\r\n";
return 1;
}
coreutils::ZString mailbox(request[2]);
coreutils::ZString password(request[3]);
bool ok = mailFileSystem.createMailbox(mailbox, password, error);
session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n";
} else if (verb == "remove") {
if (request.getList().size() < 3) {
session.out << "Usage: mailbox remove user@domain" << "\r\n";
return 1;
}
coreutils::ZString mailbox(request[2]);
bool ok = mailFileSystem.removeMailbox(mailbox, error);
session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n";
} else if (verb == "list") {
if (request.getList().size() < 3) {
session.out << "Usage: mailbox list domain" << "\r\n";
return 1;
}
coreutils::ZString domain(request[2]);
for (auto &mailbox : mailFileSystem.listMailboxes(domain))
session.out << mailbox << "@" << domain << "\r\n";
} else {
session.out << "Usage: mailbox add user@domain password | remove user@domain | list domain" << "\r\n";
}
return 1;
}
}
+25
View File
@@ -0,0 +1,25 @@
#ifndef ____ADMIN_MAILBOX_h__
# define ____ADMIN_MAILBOX_h__
# include "Command.h"
# include "MailFileSystem.h"
namespace mail {
///
/// Admin console command: "mailbox add user@domain password",
/// "mailbox remove user@domain", "mailbox list domain" - see
/// MailFileSystem::createMailbox()/removeMailbox()/listMailboxes().
///
class __ADMIN_MAILBOX : public core::Command {
public:
__ADMIN_MAILBOX(MailFileSystem &mailFileSystem);
int processCommand(coreutils::ZString &request, core::TCPSession &session) override;
private:
MailFileSystem &mailFileSystem;
};
}
#endif
+20
View File
@@ -0,0 +1,20 @@
#include "__ADMIN_PASSWORD.h"
namespace mail {
__ADMIN_PASSWORD::__ADMIN_PASSWORD(MailFileSystem &mailFileSystem) : mailFileSystem(mailFileSystem) {}
int __ADMIN_PASSWORD::processCommand(coreutils::ZString &request, core::TCPSession &session) {
if (request.getList().size() < 4 || request[1].str() != "reset") {
session.out << "Usage: password reset user@domain newpassword" << "\r\n";
return 1;
}
coreutils::ZString mailbox(request[2]);
coreutils::ZString newPassword(request[3]);
std::string error;
bool ok = mailFileSystem.setPassword(mailbox, newPassword, error);
session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n";
return 1;
}
}
+28
View File
@@ -0,0 +1,28 @@
#ifndef ____ADMIN_PASSWORD_h__
# define ____ADMIN_PASSWORD_h__
# include "Command.h"
# include "MailFileSystem.h"
namespace mail {
///
/// Admin console command: "password reset user@domain newpassword" -
/// see MailFileSystem::setPassword(). A separate top-level command
/// (rather than a verb under "mailbox") since resetting a user's
/// password was called out as its own explicit requirement, and
/// keeping it separate leaves room for other account-level
/// configuration commands later without reworking mailbox add/remove.
///
class __ADMIN_PASSWORD : public core::Command {
public:
__ADMIN_PASSWORD(MailFileSystem &mailFileSystem);
int processCommand(coreutils::ZString &request, core::TCPSession &session) override;
private:
MailFileSystem &mailFileSystem;
};
}
#endif
+53 -3
View File
@@ -12,12 +12,27 @@
#include "SendMail.h"
#include "BARANTMailConfig.h"
#include "DKIMSigner.h"
#include "__ADMIN_DOMAIN.h"
#include "__ADMIN_MAILBOX.h"
#include "__ADMIN_ALIAS.h"
#include "__ADMIN_PASSWORD.h"
#include <fcntl.h>
#include <iomanip>
#include <iostream>
#include <signal.h>
#include <sstream>
int main(int argc, char **argv) {
// Every socket write in this process (outbound relay in
// SendMail::deliverToHost(), and every client-facing SMTP/IMAP/
// POP3 session) can hit a peer that already reset the connection.
// The default SIGPIPE disposition kills the WHOLE process on the
// very next write to it - not just that one connection - which is
// exactly what repeated crash logs traced back to (a relay thread
// writing DATA to an MX host that reset mid-write, taking down
// every other in-flight connection with it). write()/send() still
// report the failure normally via EPIPE once this is ignored.
signal(SIGPIPE, SIG_IGN);
// /var/log is root:syslog-owned and not writable by an unprivileged
// user - this constructor throws immediately (before anything else
// in main() runs) rather than silently falling back, when run as
@@ -135,9 +150,44 @@ int main(int argc, char **argv) {
core::IPAddress(ipAddress, config.getImapPort()),
config.getTlsInfo());
core::ConsoleServer consoleServer(ePoll, core::IPAddress(ipAddress, 1027), nullptr);
consoleServer.commands.add(consoleServer.commands, "help");
consoleServer.commands.add(smtpServer.commands, "smtp");
// Real, authenticated domain/mailbox/alias/password administration -
// replaces what used to be an unauthenticated console that also
// exposed the raw SMTP command set (AUTH/DATA/EHLO/EXPN/HELO/...)
// to anyone who could reach the port; confirmed reachable
// externally (bound 0.0.0.0) with a login flow that never actually
// checked anything. Only started when barantmail.cfg has a real
// "admin" section configured - matching the same optional-listener
// pattern as smtpTLS/imap/pop3 above, rather than ever handing out
// an admin port with nothing to authenticate against.
core::ConsoleServer *consoleServer = nullptr;
mail::__ADMIN_DOMAIN *adminDomain = nullptr;
mail::__ADMIN_MAILBOX *adminMailbox = nullptr;
mail::__ADMIN_ALIAS *adminAlias = nullptr;
mail::__ADMIN_PASSWORD *adminPassword = nullptr;
if (config.hasAdmin()) {
std::string adminUser = config.getAdminUser();
std::string adminPasswordValue = config.getAdminPassword();
consoleServer = new core::ConsoleServer(ePoll, core::IPAddress(ipAddress, config.getAdminPort()), nullptr,
[adminUser, adminPasswordValue](std::string user, std::string password) {
return user == adminUser && password == adminPasswordValue;
});
adminDomain = new mail::__ADMIN_DOMAIN(mailFileSystem);
adminMailbox = new mail::__ADMIN_MAILBOX(mailFileSystem);
adminAlias = new mail::__ADMIN_ALIAS(mailFileSystem);
adminPassword = new mail::__ADMIN_PASSWORD(mailFileSystem);
// Command lookup upper-cases the typed keyword before matching
// against these registered names (see CommandList::processRequest) -
// every name here has to already be upper case or it can never
// be found at all, which is exactly why "help" silently did
// nothing even after CommandList::processCommand() itself was
// fixed to list commands: it was registered as lower-case "help"
// here and could never be looked up by the upper-cased key.
consoleServer->commands.add(consoleServer->commands, "HELP");
consoleServer->commands.add(*adminDomain, "DOMAIN");
consoleServer->commands.add(*adminMailbox, "MAILBOX");
consoleServer->commands.add(*adminAlias, "ALIAS");
consoleServer->commands.add(*adminPassword, "PASSWORD");
}
while(true)
sleep(3600);