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
+267 -64

No files matched your search

+27 -8
View File
@@ -82,20 +82,39 @@ namespace mail {
return hasTls ? &tlsInfo : NULL; return hasTls ? &tlsInfo : NULL;
} }
bool BARANTMailConfig::hasDkim() { std::vector<BARANTMailConfig::DkimEntry> BARANTMailConfig::getDkimEntries() {
return file->exists("dkim.domain") && file->exists("dkim.selector") && file->exists("dkim.privateKey"); std::vector<DkimEntry> entries;
// No length-prefixed array accessor on JFile/JString - probe
// ascending indices via exists() (the same technique the JSON
// path syntax's own "a[2]" bracket form is meant for) until one
// is missing, matching a plain JSON array's natural "no trailing
// comma/count field" shape.
for (int i = 0; ; i++) {
std::string indexPrefix = "dkim[" + std::to_string(i) + "]";
std::string domainPath = indexPrefix + ".domain";
if (!file->exists(domainPath))
break;
std::string selectorPath = indexPrefix + ".selector";
std::string keyPath = indexPrefix + ".privateKey";
DkimEntry entry;
entry.domain = file->getString(domainPath);
entry.selector = file->getString(selectorPath);
entry.privateKeyPath = file->getString(keyPath);
entries.push_back(entry);
}
return entries;
} }
std::string BARANTMailConfig::getDkimDomain() { bool BARANTMailConfig::hasLog() {
return file->getString("dkim.domain"); return file->exists("log");
} }
std::string BARANTMailConfig::getDkimSelector() { size_t BARANTMailConfig::getLogMaxSizeBytes() {
return file->getString("dkim.selector"); return file->exists("log.maxSizeBytes") ? (size_t)file->getInt("log.maxSizeBytes") : 104857600;
} }
std::string BARANTMailConfig::getDkimPrivateKeyPath() { int BARANTMailConfig::getLogMaxBackups() {
return file->getString("dkim.privateKey"); return file->exists("log.maxBackups") ? (int)file->getInt("log.maxBackups") : 5;
} }
bool BARANTMailConfig::hasAdmin() { bool BARANTMailConfig::hasAdmin() {
+32 -11
View File
@@ -3,6 +3,7 @@
#include "TLSInfo.h" #include "TLSInfo.h"
#include <string> #include <string>
#include <vector>
namespace coreutils { class JFile; } namespace coreutils { class JFile; }
@@ -26,11 +27,13 @@ namespace mail {
/// "key": "/path/to/privkey.pem", /// "key": "/path/to/privkey.pem",
/// "caCertificate": "/path/to/ca.pem" /// "caCertificate": "/path/to/ca.pem"
/// }, /// },
/// "dkim": { /// "dkim": [
/// "domain": "mail.example.com", /// { "domain": "example.com", "selector": "default",
/// "selector": "default", /// "privateKey": "/path/to/example.com.private.pem" },
/// "privateKey": "/path/to/dkim_private.pem" /// { "domain": "second-domain.com", "selector": "default",
/// } /// "privateKey": "/path/to/second-domain.com.private.pem" }
/// ],
/// "log": { "maxSizeBytes": 104857600, "maxBackups": 5 }
/// } /// }
/// ///
/// "hostName", "mailPath", and "smtp.port" are required - plain port /// "hostName", "mailPath", and "smtp.port" are required - plain port
@@ -43,8 +46,18 @@ namespace mail {
/// "tls.certificate" and "tls.key" become required - there is no /// "tls.certificate" and "tls.key" become required - there is no
/// path through this class that hands back a submission/IMAPS/POP3S /// path through this class that hands back a submission/IMAPS/POP3S
/// listener request without a real certificate configured for it. /// listener request without a real certificate configured for it.
/// "dkim" is entirely optional - outbound mail goes out unsigned /// "dkim" is entirely optional (an empty/missing array - outbound
/// unless all three of its fields are present. /// mail goes out unsigned for any domain with no matching entry).
/// Each entry's three fields are all required. A message is signed
/// using whichever entry's "domain" matches the sender address's
/// own domain (case-sensitive exact match) - a domain with no entry
/// here just goes out unsigned, same as if "dkim" were absent
/// entirely.
/// "log" is entirely optional - "maxSizeBytes"/"maxBackups" each
/// independently default (100MB / 5 backups) if omitted, and the
/// whole section can be left out for the same defaults. Applied to
/// coreutils::Log's static rotation fields once this config loads -
/// see Log.h's own doc comment for what rotation actually does.
/// ///
class BARANTMailConfig { class BARANTMailConfig {
@@ -78,10 +91,18 @@ namespace mail {
core::TLSInfo *getTlsInfo(); core::TLSInfo *getTlsInfo();
bool hasDkim(); struct DkimEntry {
std::string getDkimDomain(); std::string domain;
std::string getDkimSelector(); std::string selector;
std::string getDkimPrivateKeyPath(); std::string privateKeyPath;
};
/// Empty if "dkim" is absent from the config file.
std::vector<DkimEntry> getDkimEntries();
bool hasLog();
size_t getLogMaxSizeBytes();
int getLogMaxBackups();
/// ///
/// The admin console (see ConsoleServer/ConsoleSession) - "port" /// The admin console (see ConsoleServer/ConsoleSession) - "port"
BIN
View File
Binary file not shown.
+40 -7
View File
@@ -73,6 +73,35 @@ namespace mail {
uint32_t uid = resolveNumber(part); uint32_t uid = resolveNumber(part);
if (uid != 0) if (uid != 0)
result.push_back(uid); result.push_back(uid);
} else if (uidMode) {
// A UID range's boundaries are just numbers, not required
// to name an existing message's UID - RFC 3501: "A:B"
// means every message whose UID falls in [A,B], not "the
// two messages literally named A and B". resolveNumber()
// above requires an exact existing-UID match via
// folder.find(), which is correct for a bare single-UID
// reference but wrong here: a real, long-lived mailbox
// almost never has a message with UID exactly 1 once
// anything has ever been expunged, so the standard
// "select everything" idiom "UID STORE 1:* +FLAGS
// (\\Deleted)" (used by the admin empty-folder feature)
// resolved fromUid to 0 and silently skipped the whole
// range every time - confirmed live: it reported
// {"ok":true} while deleting nothing from a real
// 538-message Trash folder. Scanning directly by UID
// instead of resolving each boundary to a sequence
// position first sidesteps that entirely.
std::string fromStr = part.substr(0, colon);
std::string toStr = part.substr(colon + 1);
uint32_t fromUid = (fromStr == "*") ? (folder.size() == 0 ? 0 : folder.messages().back().uid)
: (uint32_t)std::strtoul(fromStr.c_str(), nullptr, 10);
uint32_t toUid = (toStr == "*") ? (folder.size() == 0 ? 0 : folder.messages().back().uid)
: (uint32_t)std::strtoul(toStr.c_str(), nullptr, 10);
if (fromUid > toUid)
std::swap(fromUid, toUid);
for (const auto &message : folder.messages())
if (message.uid >= fromUid && message.uid <= toUid)
result.push_back(message.uid);
} else { } else {
std::string fromStr = part.substr(0, colon); std::string fromStr = part.substr(0, colon);
std::string toStr = part.substr(colon + 1); std::string toStr = part.substr(colon + 1);
@@ -82,14 +111,18 @@ namespace mail {
continue; continue;
if (fromUid > toUid) if (fromUid > toUid)
std::swap(fromUid, toUid); std::swap(fromUid, toUid);
// Sequence-number range (plain, non-UID STORE/FETCH/etc.) -
// resolveNumber() already turned both ends into concrete // resolveNumber() already turned both ends into concrete
// UIDs regardless of uidMode - walking sequence-number- // UIDs, and unlike the uidMode branch above, a plain
// by-sequence-number (not uid-by-uid) from here is // sequence number always names a real position
// deliberate: a UID range like "5:9" must include every // (1..folder.size()), so this round-trip through
// message whose UID falls in that range, not just // folder.seq() doesn't have that branch's problem. Walking
// messages whose UID happens to equal one of the // sequence-number-by-sequence-number (not uid-by-uid) is
// integers 5-9 (UIDs aren't dense/contiguous once // still deliberate here: a range like "5:9" must include
// anything's been expunged). // every message whose UID falls in that range, not just
// messages whose UID happens to equal one of the integers
// 5-9 (UIDs aren't dense/contiguous once anything's been
// expunged).
uint32_t fromSeq = folder.seq(fromUid); uint32_t fromSeq = folder.seq(fromUid);
uint32_t toSeq = folder.seq(toUid); uint32_t toSeq = folder.seq(toUid);
if (fromSeq == 0 || toSeq == 0) if (fromSeq == 0 || toSeq == 0)
+28
View File
@@ -95,6 +95,7 @@ namespace mail {
while (std::getline(in, line)) { while (std::getline(in, line)) {
if (line.empty()) if (line.empty())
continue; continue;
try {
std::istringstream stream(line); std::istringstream stream(line);
Message message; Message message;
std::string uidStr, dateStr, flagsBlob; std::string uidStr, dateStr, flagsBlob;
@@ -105,10 +106,37 @@ namespace mail {
// consumed up to. // consumed up to.
if (!flagsBlob.empty() && flagsBlob[0] == ' ') if (!flagsBlob.empty() && flagsBlob[0] == ' ')
flagsBlob.erase(0, 1); flagsBlob.erase(0, 1);
// A short/truncated line (a write cut off mid-flush by a
// crash or restart racing save() - which already logs that
// this exact failure mode is possible, see save() below)
// leaves uidStr/dateStr empty, and stoul/stoll throw
// std::invalid_argument on that - uncaught, that previously
// propagated all the way up through load() and every IMAP
// handler that calls it (SELECT, EXAMINE) into
// CommandList::processRequest()'s own generic catch-all,
// which silently swallowed it and sent NO response at all -
// indistinguishable from the server hanging, since the
// client just waits forever for a reply that was never
// coming. Confirmed live: a single truncated trailing line
// ("Seen", missing its filename/uid/date - the tail end of
// what should have been a real message line) in a real
// user's Inbox .imapindex made every single SELECT/EXAMINE
// of that folder hang the connection, indefinitely,
// deterministically, until this line was found and
// skipped. Catching here instead means one corrupt line is
// dropped (logged) rather than taking the whole folder
// load - and therefore the whole command - down; the
// in-memory message list this produces is what save() (also
// called at the end of load(), unconditionally) rewrites
// the index file from, so the corrupt line is
// self-healed away on the very next successful load().
message.uid = (uint32_t)std::stoul(uidStr); message.uid = (uint32_t)std::stoul(uidStr);
message.internalDate = (time_t)std::stoll(dateStr); message.internalDate = (time_t)std::stoll(dateStr);
message.flags = splitFlags(flagsBlob); message.flags = splitFlags(flagsBlob);
mMessages.push_back(message); mMessages.push_back(message);
} catch (...) {
coreutils::Log(coreutils::LOG_EXCEPT) << "MailFolder: skipping malformed .imapindex line in " << path << ": [" << line << "]";
}
} }
} }
+51
View File
@@ -0,0 +1,51 @@
#ifndef __ResolverThreadScope_h__
#define __ResolverThreadScope_h__
#include "ThreadScope.h"
#include <resolv.h>
namespace mail {
///
/// ResolverThreadScope
///
/// glibc's classic resolver state (_res, touched by res_query() and
/// getaddrinfo()) is thread-LOCAL, not process-global - confirmed
/// directly (a small standalone test program): a fresh thread's own
/// _res starts zeroed, and its first real resolver call lazily
/// auto-initializes it to glibc's own compiled-in defaults
/// (RES_TIMEOUT=5s, RES_DFLRETRY=2 tries - see /usr/include/resolv.h),
/// completely ignoring anything main() set beforehand. An earlier fix
/// attempt called res_init()+set _res.retrans/_res.retry once in
/// main() before EPoll::start() spawned its worker threads - that
/// only ever affected the MAIN thread's own copy, a pure no-op for
/// every one of the 4 worker threads that actually run
/// SPFChecker/DMARCChecker's res_query()/getaddrinfo() calls for real
/// inbound mail. Same shape as MysqlThreadScope's own reason to
/// exist (see its doc comment) - a client library keying off
/// per-thread state that nothing else in this stack initializes on
/// EPoll's worker threads.
///
/// Bounding these here is what actually keeps a single slow or
/// non-responding DNS server from blocking a worker thread long
/// enough to stall every OTHER connection on every listening port
/// too (SMTP/SMTPTLS/IMAP/POP3/admin all share these same 4 threads) -
/// SPF's own "include:" mechanism can chain up to 10 sequential
/// lookups for a single message, so the per-query bound matters more
/// here than it would for an isolated one-off query.
///
class ResolverThreadScope : public core::ThreadScope {
public:
void onThreadStart() override {
res_init();
_res.retrans = 2;
_res.retry = 1;
}
};
}
#endif
+3 -3
View File
@@ -9,8 +9,8 @@ namespace mail {
SMTPServer::SMTPServer(core::EPoll &ePoll, std::string hostName, MailFileSystem &mailFileSystem, SMTPServer::SMTPServer(core::EPoll &ePoll, std::string hostName, MailFileSystem &mailFileSystem,
core::IPAddress ipAddress, core::TLSInfo *tlsInfo, bool watchQueue, core::IPAddress ipAddress, core::TLSInfo *tlsInfo, bool watchQueue,
DKIMSigner *dkimSigner) std::map<std::string, DKIMSigner*> *dkimSigners)
: core::TCPServer(ePoll, ipAddress, tlsInfo, " ", 1, "SMTP Server"), core::INotify(ePoll), ePoll(ePoll), hostName(hostName), mailFileSystem(mailFileSystem), dkimSigner(dkimSigner) { : core::TCPServer(ePoll, ipAddress, tlsInfo, " ", 1, "SMTP Server"), core::INotify(ePoll), ePoll(ePoll), hostName(hostName), mailFileSystem(mailFileSystem), dkimSigners(dkimSigners) {
commands.add(_smtp_auth, "AUTH"); commands.add(_smtp_auth, "AUTH");
commands.add(_smtp_data, "DATA"); commands.add(_smtp_data, "DATA");
commands.add(_smtp_ehlo, "EHLO"); commands.add(_smtp_ehlo, "EHLO");
@@ -114,7 +114,7 @@ namespace mail {
queuedFile.read(); queuedFile.read();
coreutils::MString mailData(queuedFile.asString()); coreutils::MString mailData(queuedFile.asString());
SendMail *outbound = new SendMail(hostName, mailFileSystem.getSendLogPath().str(), SendMail *outbound = new SendMail(hostName, mailFileSystem.getSendLogPath().str(),
name[0].str(), sender, recipient, mailData, dkimSigner); name[0].str(), sender, recipient, mailData, dkimSigners);
// The queue file's content is already fully read into // The queue file's content is already fully read into
// mailData above, so it's safe to unlink it now rather // mailData above, so it's safe to unlink it now rather
// than after the detached thread finishes (which may be // than after the detached thread finishes (which may be
+9 -6
View File
@@ -3,6 +3,7 @@
# include <string> # include <string>
# include <vector> # include <vector>
# include <map>
# include <filesystem> # include <filesystem>
# include "EPoll.h" # include "EPoll.h"
# include "TCPServer.h" # include "TCPServer.h"
@@ -38,18 +39,20 @@ namespace mail {
// each SMTPServer's own INotify watch is independent, so two // each SMTPServer's own INotify watch is independent, so two
// watchers on the same directory would each fire inCreate() for // watchers on the same directory would each fire inCreate() for
// every queued message, delivering it twice. // every queued message, delivering it twice.
// dkimSigner is optional (nullptr = outbound mail goes out // dkimSigners is optional (nullptr = outbound mail goes out
// unsigned) and not owned - passed straight through to every // unsigned for every domain) and not owned - passed straight
// SendMail this instance's inCreate() constructs for outbound // through to every SendMail this instance's inCreate() constructs
// relay. // for outbound relay, which picks the entry matching each
// individual message's own sender domain (see SendMail's own doc
// comment).
SMTPServer(core::EPoll &ePoll, std::string hostName, MailFileSystem &mailFileSystem, SMTPServer(core::EPoll &ePoll, std::string hostName, MailFileSystem &mailFileSystem,
core::IPAddress ipAddress, core::TLSInfo *tlsInfo, bool watchQueue = true, core::IPAddress ipAddress, core::TLSInfo *tlsInfo, bool watchQueue = true,
DKIMSigner *dkimSigner = nullptr); std::map<std::string, DKIMSigner*> *dkimSigners = nullptr);
MailFileSystem &mailFileSystem; MailFileSystem &mailFileSystem;
std::string hostName; std::string hostName;
core::EPoll &ePoll; core::EPoll &ePoll;
DKIMSigner *dkimSigner; std::map<std::string, DKIMSigner*> *dkimSigners;
// Advertised via EHLO's "250-SIZE <n>" and enforced by __SMTP_DATA // Advertised via EHLO's "250-SIZE <n>" and enforced by __SMTP_DATA
// while accumulating a message body - RFC 1870. 25MB, matching // while accumulating a message body - RFC 1870. 25MB, matching
+18 -5
View File
@@ -1,6 +1,7 @@
#include "SendMail.h" #include "SendMail.h"
#include "Log.h" #include "Log.h"
#include <algorithm> #include <algorithm>
#include <map>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <arpa/nameser.h> #include <arpa/nameser.h>
#include <cstdlib> #include <cstdlib>
@@ -16,9 +17,9 @@ namespace mail {
SendMail::SendMail(std::string hostName, std::string sendLogPath, std::string messageId, SendMail::SendMail(std::string hostName, std::string sendLogPath, std::string messageId,
coreutils::MString from, coreutils::MString recipient, coreutils::MString mailData, coreutils::MString from, coreutils::MString recipient, coreutils::MString mailData,
DKIMSigner *dkimSigner) std::map<std::string, DKIMSigner*> *dkimSigners)
: hostName(hostName), sendLogPath(sendLogPath), messageId(messageId), : hostName(hostName), sendLogPath(sendLogPath), messageId(messageId),
from(from), recipient(recipient), mailData(mailData), dkimSigner(dkimSigner) {} from(from), recipient(recipient), mailData(mailData), dkimSigners(dkimSigners) {}
std::string SendMail::domainOf(coreutils::MString &address) { std::string SendMail::domainOf(coreutils::MString &address) {
std::string addr = address.str(); std::string addr = address.str();
@@ -216,17 +217,29 @@ namespace mail {
coreutils::File log(logFileName, O_CREAT | O_WRONLY | O_APPEND, 0660); coreutils::File log(logFileName, O_CREAT | O_WRONLY | O_APPEND, 0660);
log.write("=== delivery attempt: message " + messageId + " -> " + recipient.str() + " ===\n"); log.write("=== delivery attempt: message " + messageId + " -> " + recipient.str() + " ===\n");
if (dkimSigner) { if (dkimSigners) {
// Signed once here, not inside deliverToHost() - deliverToHost() // Signed once here, not inside deliverToHost() - deliverToHost()
// can run again against a second MX host if the first fails, and // can run again against a second MX host if the first fails, and
// the signature (over this exact message content) is the same // the signature (over this exact message content) is the same
// either way, so signing once upfront avoids redundant RSA ops // either way, so signing once upfront avoids redundant RSA ops
// and keeps every delivery attempt for this message consistent. // and keeps every delivery attempt for this message consistent.
std::string signature = dkimSigner->sign(mailData.str()); // Which signer (if any) is used depends on the sender's own
// domain, not a single server-wide identity - a real multi-
// domain deployment must never sign one domain's mail with a
// different domain's key (DMARC alignment would fail it
// anyway, and it would leak which private key production
// uses for an unrelated domain).
std::string fromDomain = domainOf(from);
auto it = dkimSigners->find(fromDomain);
if (it != dkimSigners->end()) {
std::string signature = it->second->sign(mailData.str());
coreutils::MString signedData; coreutils::MString signedData;
signedData << signature.c_str() << mailData; signedData << signature.c_str() << mailData;
mailData = signedData; mailData = signedData;
log.write("DKIM-signed.\n"); log.write("DKIM-signed (domain: " + fromDomain + ").\n");
} else {
log.write("No DKIM key configured for domain: " + fromDomain + " - sending unsigned.\n");
}
} }
std::string domain = domainOf(recipient); std::string domain = domainOf(recipient);
+11 -6
View File
@@ -4,6 +4,7 @@
#include "DKIMSigner.h" #include "DKIMSigner.h"
#include "File.h" #include "File.h"
#include "MString.h" #include "MString.h"
#include <map>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -41,13 +42,17 @@ namespace mail {
class SendMail { class SendMail {
public: public:
// dkimSigner is optional (nullptr = don't sign) and not owned - // dkimSigners is optional (nullptr = don't sign anything) and not
// the caller keeps it alive for as long as any SendMail built // owned - the caller keeps both it and every DKIMSigner it points
// from it might still be sending (a shared signer is fine to // to alive for as long as any SendMail built from it might still
// reuse across many messages; it holds no per-message state). // be sending (shared, reusable across many messages; a signer
// holds no per-message state). Keyed by domain (matching a
// sender address's own domain, case-sensitive exact match, via
// domainOf() on `from`) - a domain with no entry just goes out
// unsigned, same as dkimSigners being nullptr entirely.
SendMail(std::string hostName, std::string sendLogPath, std::string messageId, SendMail(std::string hostName, std::string sendLogPath, std::string messageId,
coreutils::MString from, coreutils::MString recipient, coreutils::MString mailData, coreutils::MString from, coreutils::MString recipient, coreutils::MString mailData,
DKIMSigner *dkimSigner = nullptr); std::map<std::string, DKIMSigner*> *dkimSigners = nullptr);
/// Blocking. Returns true if some MX host for the recipient's /// Blocking. Returns true if some MX host for the recipient's
/// domain accepted the message, false if DNS resolution or every /// domain accepted the message, false if DNS resolution or every
@@ -68,7 +73,7 @@ namespace mail {
coreutils::MString from; coreutils::MString from;
coreutils::MString recipient; coreutils::MString recipient;
coreutils::MString mailData; coreutils::MString mailData;
DKIMSigner *dkimSigner; std::map<std::string, DKIMSigner*> *dkimSigners;
}; };
+48 -18
View File
@@ -12,6 +12,7 @@
#include "SendMail.h" #include "SendMail.h"
#include "BARANTMailConfig.h" #include "BARANTMailConfig.h"
#include "DKIMSigner.h" #include "DKIMSigner.h"
#include <map>
#include "__ADMIN_DOMAIN.h" #include "__ADMIN_DOMAIN.h"
#include "__ADMIN_MAILBOX.h" #include "__ADMIN_MAILBOX.h"
#include "__ADMIN_ALIAS.h" #include "__ADMIN_ALIAS.h"
@@ -21,6 +22,7 @@
#include <iostream> #include <iostream>
#include <signal.h> #include <signal.h>
#include <sstream> #include <sstream>
#include "ResolverThreadScope.h"
int main(int argc, char **argv) { int main(int argc, char **argv) {
// Every socket write in this process (outbound relay in // 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 // as much "outbound mail from this server" as anything queued
// through SMTP, and should be signed the same way. A missing // through SMTP, and should be signed the same way. A missing
// config file here (BARANTMailConfig's own JFile falls back to // 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. // continues unsigned, not a hard failure.
mail::DKIMSigner *dkimSigner = nullptr; std::map<std::string, mail::DKIMSigner*> *dkimSigners = nullptr;
try { try {
mail::BARANTMailConfig config("barantmail.cfg"); mail::BARANTMailConfig config("barantmail.cfg");
if (config.hasDkim()) coreutils::Log::maxLogSize = config.getLogMaxSizeBytes();
dkimSigner = new mail::DKIMSigner(config.getDkimDomain(), config.getDkimSelector(), coreutils::Log::maxLogBackups = config.getLogMaxBackups();
config.getDkimPrivateKeyPath()); 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 (...) { } catch (...) {
// Config missing hostName/mailPath/smtp.port (all required - // Config missing hostName/mailPath/smtp.port (all required -
// see BARANTMailConfig's own doc comment) - fine, this // 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; idStream << timex.tv_sec << "." << std::setfill('0') << std::setw(9) << timex.tv_nsec;
mail::SendMail mail("localhost", mailFileSystem.getSendLogPath().str(), idStream.str(), 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(); bool ok = mail.send();
coreutils::Log(coreutils::LOG_DEBUG_1) << "send result: " << (ok ? "delivered" : "failed"); coreutils::Log(coreutils::LOG_DEBUG_1) << "send result: " << (ok ? "delivered" : "failed");
std::cout << (ok ? "250 OK\n" : "550 delivery failed\n"); 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"; std::string configPath = (argc > 1) ? argv[1] : "barantmail.cfg";
mail::BARANTMailConfig config(configPath); mail::BARANTMailConfig config(configPath);
coreutils::Log::maxLogSize = config.getLogMaxSizeBytes();
coreutils::Log::maxLogBackups = config.getLogMaxBackups();
std::string hostName = config.getHostName(); std::string hostName = config.getHostName();
std::string ipAddress = "0.0.0.0"; std::string ipAddress = "0.0.0.0";
@@ -112,16 +121,37 @@ int main(int argc, char **argv) {
mail::MailFileSystem mailFileSystem(mailPath); mail::MailFileSystem mailFileSystem(mailPath);
core::EPoll ePoll; 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 // One map of signers, shared by every outbound-relaying listener -
// holds no per-message state (see DKIMSigner's own doc comment), // each DKIMSigner holds no per-message state (see its own doc
// so there's no reason for the plaintext and TLS SMTP listeners // comment), so there's no reason for the plaintext and TLS SMTP
// to each load their own copy of the same private key. // listeners to each load their own copies of the same keys. Built
mail::DKIMSigner *dkimSigner = nullptr; // once here regardless of how many domains are configured -
if (config.hasDkim()) // SendMail picks the entry matching each individual outbound
dkimSigner = new mail::DKIMSigner(config.getDkimDomain(), config.getDkimSelector(), // message's own sender domain at send time (see its own doc
config.getDkimPrivateKeyPath()); // 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 // Port 25 always runs unencrypted and always owns the mail queue
// watch (see SMTPServer's own doc comment on watchQueue) - an // 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. // needs regardless of what else is configured.
mail::SMTPServer smtpServer(ePoll, hostName, mailFileSystem, mail::SMTPServer smtpServer(ePoll, hostName, mailFileSystem,
core::IPAddress(ipAddress, config.getSmtpPort()), nullptr, true, core::IPAddress(ipAddress, config.getSmtpPort()), nullptr, true,
dkimSigner); dkimSigners);
mail::SMTPServer *smtpTLSServer = nullptr; mail::SMTPServer *smtpTLSServer = nullptr;
if (config.hasSmtpTLS()) if (config.hasSmtpTLS())
smtpTLSServer = new mail::SMTPServer(ePoll, hostName, mailFileSystem, smtpTLSServer = new mail::SMTPServer(ePoll, hostName, mailFileSystem,
core::IPAddress(ipAddress, config.getSmtpTLSPort()), core::IPAddress(ipAddress, config.getSmtpTLSPort()),
config.getTlsInfo(), false, dkimSigner); config.getTlsInfo(), false, dkimSigners);
mail::POP3Server *pop3Server = nullptr; mail::POP3Server *pop3Server = nullptr;
if (config.hasPop3()) if (config.hasPop3())
@@ -167,7 +197,7 @@ int main(int argc, char **argv) {
if (config.hasAdmin()) { if (config.hasAdmin()) {
std::string adminUser = config.getAdminUser(); std::string adminUser = config.getAdminUser();
std::string adminPasswordValue = config.getAdminPassword(); 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) { [adminUser, adminPasswordValue](std::string user, std::string password) {
return user == adminUser && password == adminPasswordValue; return user == adminUser && password == adminPasswordValue;
}); });