Add real IMAP4rev1/POP3 implementations, DKIM/DMARC/SPF, SpamAssassin delivery hook
IMAP (RFC 3501): LOGIN through UID, previously all stubs except CAPABILITY/LOGOUT/a partial APPEND. Multi-folder support with per-folder UID/flag indexing (new MailFolder class, backed by .uidvalidity/.uidnext/ .imapindex sidecar files), ENVELOPE support (new MessageHeaders class: header parsing, address-list parsing, IMAP quoting), and shared cursor-based argument parsing (new IMAPArgs class: sequence sets, flag lists, dates, folder-name normalization, and path-traversal protection via isValidFolderName - a bare ".." folder name would otherwise resolve into a sibling user's mailbox directory). POP3 (RFC 1939): USER through QUIT, previously only CAPA. Added UIDL. POP3 handlers use cursor-based parsing throughout rather than indexed access - ZString's copy constructor doesn't copy its `list` field, and POP3's dispatch passes the request by value, so indexed access into a copy was silently reading nothing. SMTP: DMARC evaluation added to the DATA-terminator path (DKIM verification + alignment/policy, Authentication-Results header, conditional rejection matching RFC 7489 6.3's p=reject + sampled pct=). AUTH LOGIN now handles RFC 4954's optional inline initial response (e.g. Python smtplib's "AUTH LOGIN <base64>" in one line) instead of only the exact three-line form. Outbound relay now forces IPv4 (AF_INET) instead of AF_UNSPEC - this server's IPv4 has a correct PTR record and SPF coverage, its IPv6 doesn't, and Gmail was rejecting outbound mail with 550 5.7.25 as a result. Delivery pipeline: local delivery now runs the message through SpamAssassin (new SpamAssassinFilter class, speaking spamd's PROCESS protocol directly over a raw socket - fails open/delivers unscanned if spamd is unreachable) instead of a plain hardlink, since scanning can tag/modify content. BARANTMailConfig: extracted config loading into its own class. compile: (see individual file changes - build script adjustments to match the above). Deliberately excluded from this commit: barantmail.log/ barantmail-service.log (operational logs, not source - barantmail.log in particular was the runaway 55GB/12.9GB file from a busy-loop bug fixed separately in ServerCore) and barantmail.cfg (this server's own live deployment config, matching how APIServer's equivalent config also stays untracked).
This commit is contained in:
1 parent
641368ea85
commit
cd5e0a9905
122 files changed
+6238
-749
No files matched your search
@@ -9,12 +9,20 @@
|
||||
#include "SMTPServer.h"
|
||||
#include "POP3Server.h"
|
||||
#include "IMAPServer.h"
|
||||
#include "SendMail2.h"
|
||||
#include "SendMail.h"
|
||||
#include "BARANTMailConfig.h"
|
||||
#include "DKIMSigner.h"
|
||||
#include <fcntl.h>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
coreutils::Log(new coreutils::File("/var/log/barantmail.log", O_WRONLY | O_APPEND | O_CREAT, 0644));
|
||||
// /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") {
|
||||
@@ -28,14 +36,44 @@ int main(int argc, char **argv) {
|
||||
index += len;
|
||||
coreutils::MString message(buffer, index);
|
||||
coreutils::Log(coreutils::LOG_DEBUG_1) << "Number of bytes read: " << index;
|
||||
core::EPoll ePoll;
|
||||
ePoll.start(1, 1000);
|
||||
mail::SendMail2 mail2(ePoll, argv[1], argv[2], message);
|
||||
mail2.send();
|
||||
coreutils::Log(coreutils::LOG_DEBUG_1) << "waiting...";
|
||||
coreutils::MString rc(mail2.wait());
|
||||
coreutils::Log(coreutils::LOG_DEBUG_1) << "rc: " << rc;
|
||||
std::cout << rc;
|
||||
|
||||
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;
|
||||
@@ -46,18 +84,58 @@ int main(int argc, char **argv) {
|
||||
|
||||
coreutils::Log(coreutils::LOG_INFO) << "BMAMail Server starting. Build " << __DATE__ << " " << __TIME__;
|
||||
|
||||
std::string hostName = "localhost";
|
||||
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";
|
||||
coreutils::ZString mailPath("/var/mail");
|
||||
// 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(1, 1000);
|
||||
mail::SMTPServer smtpServer(ePoll, hostName, mailFileSystem, core::IPAddress(ipAddress, 9025));
|
||||
// mail::POP3Server pop3Server(ePoll, hostName, mailFileSystem, core::IPAddress(ipAddress, 110));
|
||||
// mail::IMAPServer imapServer(ePoll, hostName, mailFileSystem, core::IPAddress(ipAddress, 143));
|
||||
core::ConsoleServer consoleServer(ePoll, core::IPAddress(ipAddress, 1027));
|
||||
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());
|
||||
|
||||
core::ConsoleServer consoleServer(ePoll, core::IPAddress(ipAddress, 1027), nullptr);
|
||||
consoleServer.commands.add(consoleServer.commands, "help");
|
||||
consoleServer.commands.add(smtpServer.commands, "smtp");
|
||||
|
||||
|
||||
Reference in new issue
Block a user