Add per-domain DKIM signing, admin console TLS, log rotation, and resolver timeout scoping

- DKIM signing was hardcoded to one domain; now keyed per-domain off
  the sending address, fixing mail for every domain but the original.
- Admin console (port 1027) is now TLS-wrapped instead of plaintext.
- barantmail.log now rotates by size (config-driven) instead of
  growing unbounded (previously hit 23GB).
- ResolverThreadScope.h: a <setenv> scoping fix for name resolution
  inside a <try> block that previously did not survive its own
  container.
- IMAPArgs.cpp: fixed "UID STORE 1:*" wrongly requiring UID 1 to
  exist, which silently made empty-folder operations delete nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Brad ArantandClaude Sonnet 5 committed 2026-10-06 17:39:06 +00:00
1 parent 5f3e6ed2e6
commit 407cc2e915
11 files changed
+284 -81

No files matched your search

+48 -18
View File
@@ -12,6 +12,7 @@
#include "SendMail.h"
#include "BARANTMailConfig.h"
#include "DKIMSigner.h"
#include <map>
#include "__ADMIN_DOMAIN.h"
#include "__ADMIN_MAILBOX.h"
#include "__ADMIN_ALIAS.h"
@@ -21,6 +22,7 @@
#include <iostream>
#include <signal.h>
#include <sstream>
#include "ResolverThreadScope.h"
int main(int argc, char **argv) {
// Every socket write in this process (outbound relay in
@@ -61,14 +63,19 @@ int main(int argc, char **argv) {
// as much "outbound mail from this server" as anything queued
// through SMTP, and should be signed the same way. A missing
// config file here (BARANTMailConfig's own JFile falls back to
// an empty "{}") just means hasDkim() is false and this
// an empty "{}") just means getDkimEntries() is empty and this
// continues unsigned, not a hard failure.
mail::DKIMSigner *dkimSigner = nullptr;
std::map<std::string, mail::DKIMSigner*> *dkimSigners = nullptr;
try {
mail::BARANTMailConfig config("barantmail.cfg");
if (config.hasDkim())
dkimSigner = new mail::DKIMSigner(config.getDkimDomain(), config.getDkimSelector(),
config.getDkimPrivateKeyPath());
coreutils::Log::maxLogSize = config.getLogMaxSizeBytes();
coreutils::Log::maxLogBackups = config.getLogMaxBackups();
auto dkimEntries = config.getDkimEntries();
if (!dkimEntries.empty()) {
dkimSigners = new std::map<std::string, mail::DKIMSigner*>();
for (auto &entry : dkimEntries)
(*dkimSigners)[entry.domain] = new mail::DKIMSigner(entry.domain, entry.selector, entry.privateKeyPath);
}
} catch (...) {
// Config missing hostName/mailPath/smtp.port (all required -
// see BARANTMailConfig's own doc comment) - fine, this
@@ -85,7 +92,7 @@ int main(int argc, char **argv) {
idStream << timex.tv_sec << "." << std::setfill('0') << std::setw(9) << timex.tv_nsec;
mail::SendMail mail("localhost", mailFileSystem.getSendLogPath().str(), idStream.str(),
coreutils::MString(argv[1]), coreutils::MString(argv[2]), message, dkimSigner);
coreutils::MString(argv[1]), coreutils::MString(argv[2]), message, dkimSigners);
bool ok = mail.send();
coreutils::Log(coreutils::LOG_DEBUG_1) << "send result: " << (ok ? "delivered" : "failed");
std::cout << (ok ? "250 OK\n" : "550 delivery failed\n");
@@ -101,6 +108,8 @@ int main(int argc, char **argv) {
std::string configPath = (argc > 1) ? argv[1] : "barantmail.cfg";
mail::BARANTMailConfig config(configPath);
coreutils::Log::maxLogSize = config.getLogMaxSizeBytes();
coreutils::Log::maxLogBackups = config.getLogMaxBackups();
std::string hostName = config.getHostName();
std::string ipAddress = "0.0.0.0";
@@ -112,16 +121,37 @@ int main(int argc, char **argv) {
mail::MailFileSystem mailFileSystem(mailPath);
core::EPoll ePoll;
ePoll.start(4, 1000);
// glibc's resolver state (_res, touched by res_query()/
// getaddrinfo() inside SPFChecker/DMARCChecker) is thread-local -
// confirmed directly (a standalone test program) that a fresh
// thread's _res starts zeroed and its own first resolver call
// lazily auto-initializes it to glibc's compiled-in defaults
// (RES_TIMEOUT=5s, RES_DFLRETRY=2 tries), regardless of anything
// set on a different thread beforehand. An earlier fix attempt
// set _res.retrans/_res.retry once here in main() before
// start() - a pure no-op for every one of these 4 worker
// threads, none of which ever saw it. See ResolverThreadScope's
// own doc comment for the full reasoning; same shape as
// MysqlThreadScope's per-thread mysql_thread_init() need just
// below.
static mail::ResolverThreadScope resolverThreadScope;
ePoll.start(4, 1000, &resolverThreadScope);
// One signer, shared by every outbound-relaying listener - it
// holds no per-message state (see DKIMSigner's own doc comment),
// so there's no reason for the plaintext and TLS SMTP listeners
// to each load their own copy of the same private key.
mail::DKIMSigner *dkimSigner = nullptr;
if (config.hasDkim())
dkimSigner = new mail::DKIMSigner(config.getDkimDomain(), config.getDkimSelector(),
config.getDkimPrivateKeyPath());
// One map of signers, shared by every outbound-relaying listener -
// each DKIMSigner holds no per-message state (see its own doc
// comment), so there's no reason for the plaintext and TLS SMTP
// listeners to each load their own copies of the same keys. Built
// once here regardless of how many domains are configured -
// SendMail picks the entry matching each individual outbound
// message's own sender domain at send time (see its own doc
// comment) rather than this server having one fixed identity.
std::map<std::string, mail::DKIMSigner*> *dkimSigners = nullptr;
auto dkimEntries = config.getDkimEntries();
if (!dkimEntries.empty()) {
dkimSigners = new std::map<std::string, mail::DKIMSigner*>();
for (auto &entry : dkimEntries)
(*dkimSigners)[entry.domain] = new mail::DKIMSigner(entry.domain, entry.selector, entry.privateKeyPath);
}
// Port 25 always runs unencrypted and always owns the mail queue
// watch (see SMTPServer's own doc comment on watchQueue) - an
@@ -130,13 +160,13 @@ int main(int argc, char **argv) {
// needs regardless of what else is configured.
mail::SMTPServer smtpServer(ePoll, hostName, mailFileSystem,
core::IPAddress(ipAddress, config.getSmtpPort()), nullptr, true,
dkimSigner);
dkimSigners);
mail::SMTPServer *smtpTLSServer = nullptr;
if (config.hasSmtpTLS())
smtpTLSServer = new mail::SMTPServer(ePoll, hostName, mailFileSystem,
core::IPAddress(ipAddress, config.getSmtpTLSPort()),
config.getTlsInfo(), false, dkimSigner);
config.getTlsInfo(), false, dkimSigners);
mail::POP3Server *pop3Server = nullptr;
if (config.hasPop3())
@@ -167,7 +197,7 @@ int main(int argc, char **argv) {
if (config.hasAdmin()) {
std::string adminUser = config.getAdminUser();
std::string adminPasswordValue = config.getAdminPassword();
consoleServer = new core::ConsoleServer(ePoll, core::IPAddress(ipAddress, config.getAdminPort()), nullptr,
consoleServer = new core::ConsoleServer(ePoll, core::IPAddress(ipAddress, config.getAdminPort()), config.getTlsInfo(),
[adminUser, adminPasswordValue](std::string user, std::string password) {
return user == adminUser && password == adminPasswordValue;
});