Port 1027 used to be an unauthenticated console (see ServerCore's ConsoleServer/ConsoleSession fix) that also exposed the raw SMTP command set (AUTH/DATA/EHLO/EXPN/HELO/...) to anyone who could reach it. It's now started only when barantmail.cfg has a real "admin" section configured (BARANTMailConfig::hasAdmin/getAdminPort/ getAdminUser/getAdminPassword), matching the same optional-listener pattern already used for smtpTLS/imap/pop3, and registers four real commands instead of the SMTP command table: domain add|remove|list mailbox add|remove|list alias add|remove|list password reset MailFileSystem gained the actual domain/mailbox/alias/password CRUD these commands call (createDomain/removeDomain/listDomains, createMailbox/removeMailbox/listMailboxes, createAlias/removeAlias/ listAliases, setPassword), matching its existing on-disk layout: a domain is a directory, a mailbox is a directory with Inbox/Spam/Trash and a .password file, an alias is a plain file listing target mailboxes one per line. getAliasMailboxes() was previously an unconditional stub that always returned empty (confirmed via grep to have zero callers anywhere), so an alias could be "created" but could never actually receive mail. It's now wired for real: SMTPServer::inCreate() checks ifAliasExists()/getAliasMailboxes() during local delivery and copies the same spam-scanned message to every target mailbox (extracted into a new deliverToMailbox() helper so the alias and single-mailbox delivery paths share it).
204 lines
9.8 KiB
C++
204 lines
9.8 KiB
C++
#include <string>
|
|
#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 <fcntl.h>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <signal.h>
|
|
#include <sstream>
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|