#include #include "EPoll.h" #include "ConsoleServer.h" #include "Exception.h" #include "File.h" #include "Log.h" #include "IPAddress.h" #include "MailFileSystem.h" #include "SMTPServer.h" #include "POP3Server.h" #include "IMAPServer.h" #include "SendMail.h" #include "BARANTMailConfig.h" #include "DKIMSigner.h" #include "__ADMIN_DOMAIN.h" #include "__ADMIN_MAILBOX.h" #include "__ADMIN_ALIAS.h" #include "__ADMIN_PASSWORD.h" #include #include #include #include #include int main(int argc, char **argv) { // Every socket write in this process (outbound relay in // SendMail::deliverToHost(), and every client-facing SMTP/IMAP/ // POP3 session) can hit a peer that already reset the connection. // The default SIGPIPE disposition kills the WHOLE process on the // very next write to it - not just that one connection - which is // exactly what repeated crash logs traced back to (a relay thread // writing DATA to an MX host that reset mid-write, taking down // every other in-flight connection with it). write()/send() still // report the failure normally via EPIPE once this is ignored. signal(SIGPIPE, SIG_IGN); // /var/log is root:syslog-owned and not writable by an unprivileged // user - this constructor throws immediately (before anything else // in main() runs) rather than silently falling back, when run as // the ordinary account this is actually deployed under. coreutils::Log(new coreutils::File("barantmail.log", O_WRONLY | O_APPEND | O_CREAT, 0644)); coreutils::Log(coreutils::LOG_INFO) << "BMAMAIL called with link " << argv[0]; std::string arg = argv[0]; if(arg.substr(arg.find_last_of("\\/") + 1) == "sendmail2") { try { coreutils::Log(coreutils::LOG_INFO) << "Starting Mail Injector. Build " << __DATE__ << " " << __TIME__; char buffer[1000000]; int index = 0; int len = 0; while((len = read(0, &buffer[index], 1000000)) > 0) index += len; coreutils::MString message(buffer, index); coreutils::Log(coreutils::LOG_DEBUG_1) << "Number of bytes read: " << index; coreutils::ZString mailPath("/var/mail"); mail::MailFileSystem mailFileSystem(mailPath); // Same config file the server branch defaults to - loaded here // too (this branch never reads barantmail.cfg otherwise) purely // for DKIM: mail injected locally through this CLI path is just // as much "outbound mail from this server" as anything queued // through SMTP, and should be signed the same way. A missing // config file here (BARANTMailConfig's own JFile falls back to // an empty "{}") just means hasDkim() is false and this // continues unsigned, not a hard failure. mail::DKIMSigner *dkimSigner = nullptr; try { mail::BARANTMailConfig config("barantmail.cfg"); if (config.hasDkim()) dkimSigner = new mail::DKIMSigner(config.getDkimDomain(), config.getDkimSelector(), config.getDkimPrivateKeyPath()); } catch (...) { // Config missing hostName/mailPath/smtp.port (all required - // see BARANTMailConfig's own doc comment) - fine, this // branch doesn't need any of those, only dkim is optional. } // A local, one-off message id for this injection's own log // file - matches __SMTP_DATA::generateMailFileName()'s scheme // (nanosecond timestamp) so both codepaths' logs sit in // .sendlog/ with the same naming convention. struct timespec timex; clock_gettime(CLOCK_REALTIME, &timex); std::stringstream idStream; idStream << timex.tv_sec << "." << std::setfill('0') << std::setw(9) << timex.tv_nsec; mail::SendMail mail("localhost", mailFileSystem.getSendLogPath().str(), idStream.str(), coreutils::MString(argv[1]), coreutils::MString(argv[2]), message, dkimSigner); bool ok = mail.send(); coreutils::Log(coreutils::LOG_DEBUG_1) << "send result: " << (ok ? "delivered" : "failed"); std::cout << (ok ? "250 OK\n" : "550 delivery failed\n"); } catch(coreutils::Exception exception) { std::cout << exception.text << " Error reason is '" << strerror(exception.errorNumber) << "' in file " << exception.file << " at line " << exception.line << std::endl; } } else { try { coreutils::Log(coreutils::LOG_INFO) << "BMAMail Server starting. Build " << __DATE__ << " " << __TIME__; std::string configPath = (argc > 1) ? argv[1] : "barantmail.cfg"; mail::BARANTMailConfig config(configPath); std::string hostName = config.getHostName(); std::string ipAddress = "0.0.0.0"; // ZString's std::string constructor takes a non-const reference, // which can't bind to a temporary - a named local is required. std::string mailPathStr = config.getMailPath(); coreutils::ZString mailPath(mailPathStr); mail::MailFileSystem mailFileSystem(mailPath); core::EPoll ePoll; ePoll.start(4, 1000); // 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()); // Port 25 always runs unencrypted and always owns the mail queue // watch (see SMTPServer's own doc comment on watchQueue) - an // inbound peer MTA relaying mail in has no TLS-first negotiation // to fall back on, so this is the one listener every deployment // needs regardless of what else is configured. mail::SMTPServer smtpServer(ePoll, hostName, mailFileSystem, core::IPAddress(ipAddress, config.getSmtpPort()), nullptr, true, dkimSigner); mail::SMTPServer *smtpTLSServer = nullptr; if (config.hasSmtpTLS()) smtpTLSServer = new mail::SMTPServer(ePoll, hostName, mailFileSystem, core::IPAddress(ipAddress, config.getSmtpTLSPort()), config.getTlsInfo(), false, dkimSigner); mail::POP3Server *pop3Server = nullptr; if (config.hasPop3()) pop3Server = new mail::POP3Server(ePoll, hostName, mailFileSystem, core::IPAddress(ipAddress, config.getPop3Port()), config.getTlsInfo()); mail::IMAPServer *imapServer = nullptr; if (config.hasImap()) imapServer = new mail::IMAPServer(ePoll, hostName, mailFileSystem, core::IPAddress(ipAddress, config.getImapPort()), config.getTlsInfo()); // Real, authenticated domain/mailbox/alias/password administration - // replaces what used to be an unauthenticated console that also // exposed the raw SMTP command set (AUTH/DATA/EHLO/EXPN/HELO/...) // to anyone who could reach the port; confirmed reachable // externally (bound 0.0.0.0) with a login flow that never actually // checked anything. Only started when barantmail.cfg has a real // "admin" section configured - matching the same optional-listener // pattern as smtpTLS/imap/pop3 above, rather than ever handing out // an admin port with nothing to authenticate against. core::ConsoleServer *consoleServer = nullptr; mail::__ADMIN_DOMAIN *adminDomain = nullptr; mail::__ADMIN_MAILBOX *adminMailbox = nullptr; mail::__ADMIN_ALIAS *adminAlias = nullptr; mail::__ADMIN_PASSWORD *adminPassword = nullptr; if (config.hasAdmin()) { std::string adminUser = config.getAdminUser(); std::string adminPasswordValue = config.getAdminPassword(); consoleServer = new core::ConsoleServer(ePoll, core::IPAddress(ipAddress, config.getAdminPort()), nullptr, [adminUser, adminPasswordValue](std::string user, std::string password) { return user == adminUser && password == adminPasswordValue; }); adminDomain = new mail::__ADMIN_DOMAIN(mailFileSystem); adminMailbox = new mail::__ADMIN_MAILBOX(mailFileSystem); adminAlias = new mail::__ADMIN_ALIAS(mailFileSystem); adminPassword = new mail::__ADMIN_PASSWORD(mailFileSystem); // Command lookup upper-cases the typed keyword before matching // against these registered names (see CommandList::processRequest) - // every name here has to already be upper case or it can never // be found at all, which is exactly why "help" silently did // nothing even after CommandList::processCommand() itself was // fixed to list commands: it was registered as lower-case "help" // here and could never be looked up by the upper-cased key. consoleServer->commands.add(consoleServer->commands, "HELP"); consoleServer->commands.add(*adminDomain, "DOMAIN"); consoleServer->commands.add(*adminMailbox, "MAILBOX"); consoleServer->commands.add(*adminAlias, "ALIAS"); consoleServer->commands.add(*adminPassword, "PASSWORD"); } while(true) sleep(3600); ePoll.stop(); } catch(coreutils::Exception exception) { std::cout << exception.text << " Error reason is '" << strerror(exception.errorNumber) << "' in file " << exception.file << " at line " << exception.line << std::endl; } } }