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:
1 parent
5f3e6ed2e6
commit
407cc2e915
11 files changed
+284
-81
No files matched your search
+27
-8
@@ -82,20 +82,39 @@ namespace mail {
|
||||
return hasTls ? &tlsInfo : NULL;
|
||||
}
|
||||
|
||||
bool BARANTMailConfig::hasDkim() {
|
||||
return file->exists("dkim.domain") && file->exists("dkim.selector") && file->exists("dkim.privateKey");
|
||||
std::vector<BARANTMailConfig::DkimEntry> BARANTMailConfig::getDkimEntries() {
|
||||
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() {
|
||||
return file->getString("dkim.domain");
|
||||
bool BARANTMailConfig::hasLog() {
|
||||
return file->exists("log");
|
||||
}
|
||||
|
||||
std::string BARANTMailConfig::getDkimSelector() {
|
||||
return file->getString("dkim.selector");
|
||||
size_t BARANTMailConfig::getLogMaxSizeBytes() {
|
||||
return file->exists("log.maxSizeBytes") ? (size_t)file->getInt("log.maxSizeBytes") : 104857600;
|
||||
}
|
||||
|
||||
std::string BARANTMailConfig::getDkimPrivateKeyPath() {
|
||||
return file->getString("dkim.privateKey");
|
||||
int BARANTMailConfig::getLogMaxBackups() {
|
||||
return file->exists("log.maxBackups") ? (int)file->getInt("log.maxBackups") : 5;
|
||||
}
|
||||
|
||||
bool BARANTMailConfig::hasAdmin() {
|
||||
|
||||
+32
-11
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "TLSInfo.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace coreutils { class JFile; }
|
||||
|
||||
@@ -26,11 +27,13 @@ namespace mail {
|
||||
/// "key": "/path/to/privkey.pem",
|
||||
/// "caCertificate": "/path/to/ca.pem"
|
||||
/// },
|
||||
/// "dkim": {
|
||||
/// "domain": "mail.example.com",
|
||||
/// "selector": "default",
|
||||
/// "privateKey": "/path/to/dkim_private.pem"
|
||||
/// }
|
||||
/// "dkim": [
|
||||
/// { "domain": "example.com", "selector": "default",
|
||||
/// "privateKey": "/path/to/example.com.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
|
||||
@@ -43,8 +46,18 @@ namespace mail {
|
||||
/// "tls.certificate" and "tls.key" become required - there is no
|
||||
/// path through this class that hands back a submission/IMAPS/POP3S
|
||||
/// listener request without a real certificate configured for it.
|
||||
/// "dkim" is entirely optional - outbound mail goes out unsigned
|
||||
/// unless all three of its fields are present.
|
||||
/// "dkim" is entirely optional (an empty/missing array - outbound
|
||||
/// 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 {
|
||||
@@ -78,10 +91,18 @@ namespace mail {
|
||||
|
||||
core::TLSInfo *getTlsInfo();
|
||||
|
||||
bool hasDkim();
|
||||
std::string getDkimDomain();
|
||||
std::string getDkimSelector();
|
||||
std::string getDkimPrivateKeyPath();
|
||||
struct DkimEntry {
|
||||
std::string domain;
|
||||
std::string selector;
|
||||
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"
|
||||
|
||||
+40
-7
@@ -73,6 +73,35 @@ namespace mail {
|
||||
uint32_t uid = resolveNumber(part);
|
||||
if (uid != 0)
|
||||
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 {
|
||||
std::string fromStr = part.substr(0, colon);
|
||||
std::string toStr = part.substr(colon + 1);
|
||||
@@ -82,14 +111,18 @@ namespace mail {
|
||||
continue;
|
||||
if (fromUid > toUid)
|
||||
std::swap(fromUid, toUid);
|
||||
// Sequence-number range (plain, non-UID STORE/FETCH/etc.) -
|
||||
// resolveNumber() already turned both ends into concrete
|
||||
// UIDs regardless of uidMode - walking sequence-number-
|
||||
// by-sequence-number (not uid-by-uid) from here is
|
||||
// deliberate: a UID range like "5:9" must include 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).
|
||||
// UIDs, and unlike the uidMode branch above, a plain
|
||||
// sequence number always names a real position
|
||||
// (1..folder.size()), so this round-trip through
|
||||
// folder.seq() doesn't have that branch's problem. Walking
|
||||
// sequence-number-by-sequence-number (not uid-by-uid) is
|
||||
// still deliberate here: a range like "5:9" must include
|
||||
// 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 toSeq = folder.seq(toUid);
|
||||
if (fromSeq == 0 || toSeq == 0)
|
||||
|
||||
+42
-14
@@ -95,20 +95,48 @@ namespace mail {
|
||||
while (std::getline(in, line)) {
|
||||
if (line.empty())
|
||||
continue;
|
||||
std::istringstream stream(line);
|
||||
Message message;
|
||||
std::string uidStr, dateStr, flagsBlob;
|
||||
stream >> message.fileName >> uidStr >> dateStr;
|
||||
std::getline(stream, flagsBlob);
|
||||
// The flags blob (if any) is whatever's left after the third
|
||||
// field, minus the single separating space istream>> already
|
||||
// consumed up to.
|
||||
if (!flagsBlob.empty() && flagsBlob[0] == ' ')
|
||||
flagsBlob.erase(0, 1);
|
||||
message.uid = (uint32_t)std::stoul(uidStr);
|
||||
message.internalDate = (time_t)std::stoll(dateStr);
|
||||
message.flags = splitFlags(flagsBlob);
|
||||
mMessages.push_back(message);
|
||||
try {
|
||||
std::istringstream stream(line);
|
||||
Message message;
|
||||
std::string uidStr, dateStr, flagsBlob;
|
||||
stream >> message.fileName >> uidStr >> dateStr;
|
||||
std::getline(stream, flagsBlob);
|
||||
// The flags blob (if any) is whatever's left after the third
|
||||
// field, minus the single separating space istream>> already
|
||||
// consumed up to.
|
||||
if (!flagsBlob.empty() && flagsBlob[0] == ' ')
|
||||
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.internalDate = (time_t)std::stoll(dateStr);
|
||||
message.flags = splitFlags(flagsBlob);
|
||||
mMessages.push_back(message);
|
||||
} catch (...) {
|
||||
coreutils::Log(coreutils::LOG_EXCEPT) << "MailFolder: skipping malformed .imapindex line in " << path << ": [" << line << "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -9,8 +9,8 @@ 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) {
|
||||
std::map<std::string, DKIMSigner*> *dkimSigners)
|
||||
: 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_data, "DATA");
|
||||
commands.add(_smtp_ehlo, "EHLO");
|
||||
@@ -114,7 +114,7 @@ namespace mail {
|
||||
queuedFile.read();
|
||||
coreutils::MString mailData(queuedFile.asString());
|
||||
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
|
||||
// mailData above, so it's safe to unlink it now rather
|
||||
// than after the detached thread finishes (which may be
|
||||
|
||||
+9
-6
@@ -3,6 +3,7 @@
|
||||
|
||||
# include <string>
|
||||
# include <vector>
|
||||
# include <map>
|
||||
# include <filesystem>
|
||||
# include "EPoll.h"
|
||||
# include "TCPServer.h"
|
||||
@@ -38,18 +39,20 @@ namespace mail {
|
||||
// each SMTPServer's own INotify watch is independent, so two
|
||||
// watchers on the same directory would each fire inCreate() for
|
||||
// every queued message, delivering it twice.
|
||||
// dkimSigner is optional (nullptr = outbound mail goes out
|
||||
// unsigned) and not owned - passed straight through to every
|
||||
// SendMail this instance's inCreate() constructs for outbound
|
||||
// relay.
|
||||
// dkimSigners is optional (nullptr = outbound mail goes out
|
||||
// unsigned for every domain) and not owned - passed straight
|
||||
// through to every SendMail this instance's inCreate() constructs
|
||||
// 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,
|
||||
core::IPAddress ipAddress, core::TLSInfo *tlsInfo, bool watchQueue = true,
|
||||
DKIMSigner *dkimSigner = nullptr);
|
||||
std::map<std::string, DKIMSigner*> *dkimSigners = nullptr);
|
||||
|
||||
MailFileSystem &mailFileSystem;
|
||||
std::string hostName;
|
||||
core::EPoll &ePoll;
|
||||
DKIMSigner *dkimSigner;
|
||||
std::map<std::string, DKIMSigner*> *dkimSigners;
|
||||
|
||||
// Advertised via EHLO's "250-SIZE <n>" and enforced by __SMTP_DATA
|
||||
// while accumulating a message body - RFC 1870. 25MB, matching
|
||||
|
||||
+21
-8
@@ -1,6 +1,7 @@
|
||||
#include "SendMail.h"
|
||||
#include "Log.h"
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <arpa/inet.h>
|
||||
#include <arpa/nameser.h>
|
||||
#include <cstdlib>
|
||||
@@ -16,9 +17,9 @@ namespace mail {
|
||||
|
||||
SendMail::SendMail(std::string hostName, std::string sendLogPath, std::string messageId,
|
||||
coreutils::MString from, coreutils::MString recipient, coreutils::MString mailData,
|
||||
DKIMSigner *dkimSigner)
|
||||
std::map<std::string, DKIMSigner*> *dkimSigners)
|
||||
: 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 addr = address.str();
|
||||
@@ -216,17 +217,29 @@ namespace mail {
|
||||
coreutils::File log(logFileName, O_CREAT | O_WRONLY | O_APPEND, 0660);
|
||||
log.write("=== delivery attempt: message " + messageId + " -> " + recipient.str() + " ===\n");
|
||||
|
||||
if (dkimSigner) {
|
||||
if (dkimSigners) {
|
||||
// Signed once here, not inside deliverToHost() - deliverToHost()
|
||||
// can run again against a second MX host if the first fails, and
|
||||
// the signature (over this exact message content) is the same
|
||||
// either way, so signing once upfront avoids redundant RSA ops
|
||||
// and keeps every delivery attempt for this message consistent.
|
||||
std::string signature = dkimSigner->sign(mailData.str());
|
||||
coreutils::MString signedData;
|
||||
signedData << signature.c_str() << mailData;
|
||||
mailData = signedData;
|
||||
log.write("DKIM-signed.\n");
|
||||
// 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;
|
||||
signedData << signature.c_str() << mailData;
|
||||
mailData = signedData;
|
||||
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);
|
||||
|
||||
+11
-6
@@ -4,6 +4,7 @@
|
||||
#include "DKIMSigner.h"
|
||||
#include "File.h"
|
||||
#include "MString.h"
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -41,13 +42,17 @@ namespace mail {
|
||||
class SendMail {
|
||||
|
||||
public:
|
||||
// dkimSigner is optional (nullptr = don't sign) and not owned -
|
||||
// the caller keeps it alive for as long as any SendMail built
|
||||
// from it might still be sending (a shared signer is fine to
|
||||
// reuse across many messages; it holds no per-message state).
|
||||
// dkimSigners is optional (nullptr = don't sign anything) and not
|
||||
// owned - the caller keeps both it and every DKIMSigner it points
|
||||
// to alive for as long as any SendMail built from it might still
|
||||
// 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,
|
||||
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
|
||||
/// domain accepted the message, false if DNS resolution or every
|
||||
@@ -68,7 +73,7 @@ namespace mail {
|
||||
coreutils::MString from;
|
||||
coreutils::MString recipient;
|
||||
coreutils::MString mailData;
|
||||
DKIMSigner *dkimSigner;
|
||||
std::map<std::string, DKIMSigner*> *dkimSigners;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user