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;
|
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
@@ -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"
|
||||||
|
|||||||
+40
-7
@@ -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)
|
||||||
|
|||||||
+42
-14
@@ -95,20 +95,48 @@ namespace mail {
|
|||||||
while (std::getline(in, line)) {
|
while (std::getline(in, line)) {
|
||||||
if (line.empty())
|
if (line.empty())
|
||||||
continue;
|
continue;
|
||||||
std::istringstream stream(line);
|
try {
|
||||||
Message message;
|
std::istringstream stream(line);
|
||||||
std::string uidStr, dateStr, flagsBlob;
|
Message message;
|
||||||
stream >> message.fileName >> uidStr >> dateStr;
|
std::string uidStr, dateStr, flagsBlob;
|
||||||
std::getline(stream, flagsBlob);
|
stream >> message.fileName >> uidStr >> dateStr;
|
||||||
// The flags blob (if any) is whatever's left after the third
|
std::getline(stream, flagsBlob);
|
||||||
// field, minus the single separating space istream>> already
|
// The flags blob (if any) is whatever's left after the third
|
||||||
// consumed up to.
|
// field, minus the single separating space istream>> already
|
||||||
if (!flagsBlob.empty() && flagsBlob[0] == ' ')
|
// consumed up to.
|
||||||
flagsBlob.erase(0, 1);
|
if (!flagsBlob.empty() && flagsBlob[0] == ' ')
|
||||||
message.uid = (uint32_t)std::stoul(uidStr);
|
flagsBlob.erase(0, 1);
|
||||||
message.internalDate = (time_t)std::stoll(dateStr);
|
// A short/truncated line (a write cut off mid-flush by a
|
||||||
message.flags = splitFlags(flagsBlob);
|
// crash or restart racing save() - which already logs that
|
||||||
mMessages.push_back(message);
|
// 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,
|
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
@@ -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
|
||||||
|
|||||||
+21
-8
@@ -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
|
||||||
coreutils::MString signedData;
|
// domain, not a single server-wide identity - a real multi-
|
||||||
signedData << signature.c_str() << mailData;
|
// domain deployment must never sign one domain's mail with a
|
||||||
mailData = signedData;
|
// different domain's key (DMARC alignment would fail it
|
||||||
log.write("DKIM-signed.\n");
|
// 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);
|
std::string domain = domainOf(recipient);
|
||||||
|
|||||||
+11
-6
@@ -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;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user