- 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>
234 lines
12 KiB
C++
234 lines
12 KiB
C++
#include <string>
|
|
#include "EPoll.h"
|
|
#include "ConsoleServer.h"
|
|
#include "Exception.h"
|
|
#include "File.h"
|
|
#include "Log.h"
|
|
#include "IPAddress.h"
|
|
#include "MailFileSystem.h"
|
|
#include "SMTPServer.h"
|
|
#include "POP3Server.h"
|
|
#include "IMAPServer.h"
|
|
#include "SendMail.h"
|
|
#include "BARANTMailConfig.h"
|
|
#include "DKIMSigner.h"
|
|
#include <map>
|
|
#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>
|
|
#include "ResolverThreadScope.h"
|
|
|
|
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
|
|
// the ordinary account this is actually deployed under.
|
|
coreutils::Log(new coreutils::File("barantmail.log", O_WRONLY | O_APPEND | O_CREAT, 0644));
|
|
coreutils::Log(coreutils::LOG_INFO) << "BMAMAIL called with link " << argv[0];
|
|
std::string arg = argv[0];
|
|
if(arg.substr(arg.find_last_of("\\/") + 1) == "sendmail2") {
|
|
try {
|
|
coreutils::Log(coreutils::LOG_INFO) << "Starting Mail Injector. Build " << __DATE__ << " " << __TIME__;
|
|
|
|
char buffer[1000000];
|
|
int index = 0;
|
|
int len = 0;
|
|
while((len = read(0, &buffer[index], 1000000)) > 0)
|
|
index += len;
|
|
coreutils::MString message(buffer, index);
|
|
coreutils::Log(coreutils::LOG_DEBUG_1) << "Number of bytes read: " << index;
|
|
|
|
coreutils::ZString mailPath("/var/mail");
|
|
mail::MailFileSystem mailFileSystem(mailPath);
|
|
|
|
// Same config file the server branch defaults to - loaded here
|
|
// too (this branch never reads barantmail.cfg otherwise) purely
|
|
// for DKIM: mail injected locally through this CLI path is just
|
|
// 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 getDkimEntries() is empty and this
|
|
// continues unsigned, not a hard failure.
|
|
std::map<std::string, mail::DKIMSigner*> *dkimSigners = nullptr;
|
|
try {
|
|
mail::BARANTMailConfig config("barantmail.cfg");
|
|
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
|
|
// branch doesn't need any of those, only dkim is optional.
|
|
}
|
|
|
|
// A local, one-off message id for this injection's own log
|
|
// file - matches __SMTP_DATA::generateMailFileName()'s scheme
|
|
// (nanosecond timestamp) so both codepaths' logs sit in
|
|
// .sendlog/ with the same naming convention.
|
|
struct timespec timex;
|
|
clock_gettime(CLOCK_REALTIME, &timex);
|
|
std::stringstream idStream;
|
|
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, 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");
|
|
}
|
|
catch(coreutils::Exception exception) {
|
|
std::cout << exception.text << " Error reason is '" << strerror(exception.errorNumber) << "' in file " << exception.file << " at line " << exception.line << std::endl;
|
|
}
|
|
} else {
|
|
|
|
try {
|
|
|
|
coreutils::Log(coreutils::LOG_INFO) << "BMAMail Server starting. Build " << __DATE__ << " " << __TIME__;
|
|
|
|
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";
|
|
// ZString's std::string constructor takes a non-const reference,
|
|
// which can't bind to a temporary - a named local is required.
|
|
std::string mailPathStr = config.getMailPath();
|
|
coreutils::ZString mailPath(mailPathStr);
|
|
|
|
mail::MailFileSystem mailFileSystem(mailPath);
|
|
|
|
core::EPoll ePoll;
|
|
// 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 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
|
|
// inbound peer MTA relaying mail in has no TLS-first negotiation
|
|
// to fall back on, so this is the one listener every deployment
|
|
// needs regardless of what else is configured.
|
|
mail::SMTPServer smtpServer(ePoll, hostName, mailFileSystem,
|
|
core::IPAddress(ipAddress, config.getSmtpPort()), nullptr, true,
|
|
dkimSigners);
|
|
|
|
mail::SMTPServer *smtpTLSServer = nullptr;
|
|
if (config.hasSmtpTLS())
|
|
smtpTLSServer = new mail::SMTPServer(ePoll, hostName, mailFileSystem,
|
|
core::IPAddress(ipAddress, config.getSmtpTLSPort()),
|
|
config.getTlsInfo(), false, dkimSigners);
|
|
|
|
mail::POP3Server *pop3Server = nullptr;
|
|
if (config.hasPop3())
|
|
pop3Server = new mail::POP3Server(ePoll, hostName, mailFileSystem,
|
|
core::IPAddress(ipAddress, config.getPop3Port()),
|
|
config.getTlsInfo());
|
|
|
|
mail::IMAPServer *imapServer = nullptr;
|
|
if (config.hasImap())
|
|
imapServer = new mail::IMAPServer(ePoll, hostName, mailFileSystem,
|
|
core::IPAddress(ipAddress, config.getImapPort()),
|
|
config.getTlsInfo());
|
|
|
|
// 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()), config.getTlsInfo(),
|
|
[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);
|
|
|
|
ePoll.stop();
|
|
|
|
}
|
|
|
|
catch(coreutils::Exception exception) {
|
|
std::cout << exception.text << " Error reason is '" << strerror(exception.errorNumber) << "' in file " << exception.file << " at line " << exception.line << std::endl;
|
|
}
|
|
}
|
|
}
|