diff --git a/BARANTMailConfig.cpp b/BARANTMailConfig.cpp index 96299e3..60e293b 100644 --- a/BARANTMailConfig.cpp +++ b/BARANTMailConfig.cpp @@ -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::getDkimEntries() { + std::vector 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() { diff --git a/BARANTMailConfig.h b/BARANTMailConfig.h index fbec7a5..481ada0 100644 --- a/BARANTMailConfig.h +++ b/BARANTMailConfig.h @@ -3,6 +3,7 @@ #include "TLSInfo.h" #include +#include 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 getDkimEntries(); + + bool hasLog(); + size_t getLogMaxSizeBytes(); + int getLogMaxBackups(); /// /// The admin console (see ConsoleServer/ConsoleSession) - "port" diff --git a/BMAMail b/BMAMail index 7b5aaf5..1045938 100755 Binary files a/BMAMail and b/BMAMail differ diff --git a/IMAPArgs.cpp b/IMAPArgs.cpp index 7c15594..fc73e5d 100644 --- a/IMAPArgs.cpp +++ b/IMAPArgs.cpp @@ -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) diff --git a/MailFolder.cpp b/MailFolder.cpp index 45f004c..a68472f 100644 --- a/MailFolder.cpp +++ b/MailFolder.cpp @@ -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 << "]"; + } } } diff --git a/ResolverThreadScope.h b/ResolverThreadScope.h new file mode 100644 index 0000000..c344da5 --- /dev/null +++ b/ResolverThreadScope.h @@ -0,0 +1,51 @@ +#ifndef __ResolverThreadScope_h__ +#define __ResolverThreadScope_h__ + +#include "ThreadScope.h" +#include + +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 diff --git a/SMTPServer.cpp b/SMTPServer.cpp index f835a7c..f2a175b 100644 --- a/SMTPServer.cpp +++ b/SMTPServer.cpp @@ -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 *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 diff --git a/SMTPServer.h b/SMTPServer.h index 989cd81..f07683c 100644 --- a/SMTPServer.h +++ b/SMTPServer.h @@ -3,6 +3,7 @@ # include # include +# include # include # 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 *dkimSigners = nullptr); MailFileSystem &mailFileSystem; std::string hostName; core::EPoll &ePoll; - DKIMSigner *dkimSigner; + std::map *dkimSigners; // Advertised via EHLO's "250-SIZE " and enforced by __SMTP_DATA // while accumulating a message body - RFC 1870. 25MB, matching diff --git a/SendMail.cpp b/SendMail.cpp index ef47b49..13d8821 100644 --- a/SendMail.cpp +++ b/SendMail.cpp @@ -1,6 +1,7 @@ #include "SendMail.h" #include "Log.h" #include +#include #include #include #include @@ -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 *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); diff --git a/SendMail.h b/SendMail.h index a8b4943..aa022a0 100644 --- a/SendMail.h +++ b/SendMail.h @@ -4,6 +4,7 @@ #include "DKIMSigner.h" #include "File.h" #include "MString.h" +#include #include #include @@ -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 *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 *dkimSigners; }; diff --git a/main.cpp b/main.cpp index 844d0f6..9557d59 100644 --- a/main.cpp +++ b/main.cpp @@ -12,6 +12,7 @@ #include "SendMail.h" #include "BARANTMailConfig.h" #include "DKIMSigner.h" +#include #include "__ADMIN_DOMAIN.h" #include "__ADMIN_MAILBOX.h" #include "__ADMIN_ALIAS.h" @@ -21,6 +22,7 @@ #include #include #include +#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 *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(); + 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 *dkimSigners = nullptr; + auto dkimEntries = config.getDkimEntries(); + if (!dkimEntries.empty()) { + dkimSigners = new std::map(); + 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; });