diff --git a/.gitignore b/.gitignore index 60fc7e9..d568c22 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ Release *~ *.mk libBMAMail.a +barantmail.cfg +*.log diff --git a/BARANTMailConfig.cpp b/BARANTMailConfig.cpp index 735f7a5..96299e3 100644 --- a/BARANTMailConfig.cpp +++ b/BARANTMailConfig.cpp @@ -14,6 +14,12 @@ namespace mail { if (!file->exists("smtp.port")) throw coreutils::Exception("BARANTMailConfig: \"smtp.port\" is missing from " + path); + if ((file->exists("admin.user") || file->exists("admin.password") || file->exists("admin.port")) && + (!file->exists("admin.user") || !file->exists("admin.password"))) + throw coreutils::Exception( + "BARANTMailConfig: \"admin\" is configured but \"admin.user\"/\"admin.password\" " + "is missing from " + path); + bool needsTls = file->exists("smtpTLS.port") || file->exists("imap.port") || file->exists("pop3.port"); if (needsTls) { if (!file->exists("tls.certificate") || !file->exists("tls.key")) @@ -92,4 +98,20 @@ namespace mail { return file->getString("dkim.privateKey"); } + bool BARANTMailConfig::hasAdmin() { + return file->exists("admin.user"); + } + + int BARANTMailConfig::getAdminPort() { + return file->exists("admin.port") ? (int)file->getInt("admin.port") : 1027; + } + + std::string BARANTMailConfig::getAdminUser() { + return file->getString("admin.user"); + } + + std::string BARANTMailConfig::getAdminPassword() { + return file->getString("admin.password"); + } + } diff --git a/BARANTMailConfig.h b/BARANTMailConfig.h index 0996a21..fbec7a5 100644 --- a/BARANTMailConfig.h +++ b/BARANTMailConfig.h @@ -83,6 +83,19 @@ namespace mail { std::string getDkimSelector(); std::string getDkimPrivateKeyPath(); + /// + /// The admin console (see ConsoleServer/ConsoleSession) - "port" + /// defaults to 1027 if omitted. "user"/"password" are required + /// once "admin" is present at all; there is no path through this + /// class that hands back an admin listener with nothing to check + /// logins against. + /// + + bool hasAdmin(); + int getAdminPort(); + std::string getAdminUser(); + std::string getAdminPassword(); + private: coreutils::JFile *file; diff --git a/BMAMail b/BMAMail index 91766f6..7b5aaf5 100755 Binary files a/BMAMail and b/BMAMail differ diff --git a/MailFileSystem.cpp b/MailFileSystem.cpp index 2ab968c..0846a8c 100644 --- a/MailFileSystem.cpp +++ b/MailFileSystem.cpp @@ -4,6 +4,7 @@ # include "Log.h" # include "File.h" # include +# include namespace mail { @@ -51,7 +52,27 @@ namespace mail { } std::vector MailFileSystem::getAliasMailboxes(coreutils::ZString &alias) { + // Previously an unconditional stub - always returned empty, + // confirmed by grep to have zero callers anywhere until this + // change wired real aliasing into SMTPServer::inCreate() and the + // admin console. The file format is this class's own choice + // (createAlias() below writes it): one target mailbox per line. std::vector returnValue; + coreutils::MString path = getMailBoxPath(alias); + try { + coreutils::File aliasFile(path, O_RDONLY); + aliasFile.read(); + coreutils::ZString contents = aliasFile.asZString(); + contents.split("\n"); + for (auto &line : contents.getList()) { + coreutils::MString target(line); + if (!target.str().empty()) + returnValue.push_back(target); + } + } catch (...) { + // Not a real alias (or unreadable) - caller sees an empty + // target list, same as before this method did anything. + } return returnValue; } @@ -113,4 +134,194 @@ namespace mail { return folders; } + bool MailFileSystem::createDomain(coreutils::ZString &domain, std::string &error) { + coreutils::MString path; + path << mailPath << "/" << domain; + if (mkdir(path.c_str(), 0770) == -1 && errno != EEXIST) { + error = "could not create domain directory"; + return false; + } + return true; + } + + bool MailFileSystem::removeDomain(coreutils::ZString &domain, std::string &error) { + coreutils::MString path; + path << mailPath << "/" << domain; + if (!std::filesystem::is_directory(path.str())) { + error = "domain does not exist"; + return false; + } + // Refuse rather than cascade-delete every mailbox/alias under a + // domain silently - the admin has to clear those first, same + // spirit as removeMailbox() not being implicitly called from here. + for (const auto &entry : std::filesystem::directory_iterator(path.str())) { + std::string name = entry.path().filename().string(); + if (!name.empty() && name[0] != '.') { + error = "domain still has mailboxes or aliases - remove those first"; + return false; + } + } + std::error_code ec; + std::filesystem::remove_all(path.str(), ec); + if (ec) { + error = "could not remove domain directory"; + return false; + } + return true; + } + + std::vector MailFileSystem::listDomains() { + std::vector domains; + std::string root = mailPath.str(); + if (!std::filesystem::is_directory(root)) + return domains; + for (const auto &entry : std::filesystem::directory_iterator(root)) { + std::string name = entry.path().filename().string(); + if (!name.empty() && name[0] != '.' && entry.is_directory()) + domains.push_back(coreutils::MString(name)); + } + return domains; + } + + bool MailFileSystem::createMailbox(coreutils::ZString &mailbox, coreutils::ZString &password, std::string &error) { + mailbox.split("@", 1); + if (mailbox.getList().size() < 2) { + error = "mailbox must be in the form user@domain"; + return false; + } + coreutils::MString domainPath; + domainPath << mailPath << "/" << mailbox[1]; + if (!std::filesystem::is_directory(domainPath.str())) { + error = "domain does not exist: " + mailbox[1].str(); + return false; + } + coreutils::MString path = getMailBoxPath(mailbox); + if (std::filesystem::exists(path.str())) { + error = "a mailbox or alias already exists at that address"; + return false; + } + if (mkdir(path.c_str(), 0770) == -1) { + error = "could not create mailbox directory"; + return false; + } + for (std::string folder : {"Inbox", "Spam", "Trash"}) { + coreutils::MString folderPath; + folderPath << path << "/" << folder; + mkdir(folderPath.c_str(), 0770); + } + coreutils::MString passwordPath; + passwordPath << path << "/.password"; + try { + coreutils::File secret(passwordPath, O_CREAT | O_WRONLY | O_TRUNC, 0660); + secret.write(password.str()); + } catch (...) { + error = "could not write password file"; + return false; + } + return true; + } + + bool MailFileSystem::removeMailbox(coreutils::ZString &mailbox, std::string &error) { + if (!ifMailBoxExists(mailbox)) { + error = "mailbox does not exist"; + return false; + } + coreutils::MString path = getMailBoxPath(mailbox); + std::error_code ec; + std::filesystem::remove_all(path.str(), ec); + if (ec) { + error = "could not remove mailbox"; + return false; + } + return true; + } + + std::vector MailFileSystem::listMailboxes(coreutils::ZString &domain) { + std::vector mailboxes; + coreutils::MString root; + root << mailPath << "/" << domain; + if (!std::filesystem::is_directory(root.str())) + return mailboxes; + for (const auto &entry : std::filesystem::directory_iterator(root.str())) { + std::string name = entry.path().filename().string(); + if (!name.empty() && name[0] != '.' && entry.is_directory()) + mailboxes.push_back(coreutils::MString(name)); + } + return mailboxes; + } + + bool MailFileSystem::createAlias(coreutils::ZString &alias, coreutils::ZString &targets, std::string &error) { + if (ifMailBoxExists(alias)) { + error = "a mailbox already exists at that address"; + return false; + } + coreutils::MString path = getMailBoxPath(alias); + if (path.str().empty()) { + error = "alias must be in the form user@domain"; + return false; + } + // Accept either comma- or whitespace-separated targets from the + // console command line - normalized here to one per line, which + // is this class's own on-disk format (see getAliasMailboxes()). + targets.split(","); + coreutils::MString contents; + for (auto &target : targets.getList()) { + coreutils::MString trimmed(target); + if (!trimmed.str().empty()) + contents << trimmed << "\n"; + } + try { + coreutils::File aliasFile(path, O_CREAT | O_WRONLY | O_TRUNC, 0660); + aliasFile.write(contents.str()); + } catch (...) { + error = "could not write alias file"; + return false; + } + return true; + } + + bool MailFileSystem::removeAlias(coreutils::ZString &alias, std::string &error) { + if (!ifAliasExists(alias)) { + error = "alias does not exist"; + return false; + } + coreutils::MString path = getMailBoxPath(alias); + if (unlink(path.c_str()) == -1) { + error = "could not remove alias file"; + return false; + } + return true; + } + + std::vector MailFileSystem::listAliases(coreutils::ZString &domain) { + std::vector aliases; + coreutils::MString root; + root << mailPath << "/" << domain; + if (!std::filesystem::is_directory(root.str())) + return aliases; + for (const auto &entry : std::filesystem::directory_iterator(root.str())) { + std::string name = entry.path().filename().string(); + if (!name.empty() && name[0] != '.' && entry.is_regular_file()) + aliases.push_back(coreutils::MString(name)); + } + return aliases; + } + + bool MailFileSystem::setPassword(coreutils::ZString &mailbox, coreutils::ZString &newPassword, std::string &error) { + if (!ifMailBoxExists(mailbox)) { + error = "mailbox does not exist"; + return false; + } + coreutils::MString path; + path << getMailBoxPath(mailbox) << "/.password"; + try { + coreutils::File secret(path, O_CREAT | O_WRONLY | O_TRUNC, 0660); + secret.write(newPassword.str()); + } catch (...) { + error = "could not write password file"; + return false; + } + return true; + } + } diff --git a/MailFileSystem.h b/MailFileSystem.h index 369af1c..c18d388 100644 --- a/MailFileSystem.h +++ b/MailFileSystem.h @@ -40,6 +40,33 @@ namespace mail { coreutils::MString getFolderPath(coreutils::ZString &mailbox, std::string folder); bool ifFolderExists(coreutils::ZString &mailbox, std::string folder); + // Domain/mailbox/alias maintenance for the admin console (see + // __ADMIN_DOMAIN/__ADMIN_MAILBOX/__ADMIN_ALIAS) - a domain is + // just a directory directly under mailPath; a mailbox is a + // directory under that with Inbox/Spam/Trash and a .password + // file (matching checkPassword()'s own expectations exactly); + // an alias is a plain FILE at that same level (matching + // ifAliasExists()'s own S_ISREG check) listing its target + // mailboxes one per line. Each returns false and fills `error` + // with a human-readable reason on failure, rather than throwing - + // the console commands calling these want to report a clean + // failure line to the operator, not crash the whole server on a + // typo'd domain name. + + bool createDomain(coreutils::ZString &domain, std::string &error); + bool removeDomain(coreutils::ZString &domain, std::string &error); + std::vector listDomains(); + + bool createMailbox(coreutils::ZString &mailbox, coreutils::ZString &password, std::string &error); + bool removeMailbox(coreutils::ZString &mailbox, std::string &error); + std::vector listMailboxes(coreutils::ZString &domain); + + bool createAlias(coreutils::ZString &alias, coreutils::ZString &targets, std::string &error); + bool removeAlias(coreutils::ZString &alias, std::string &error); + std::vector listAliases(coreutils::ZString &domain); + + bool setPassword(coreutils::ZString &mailbox, coreutils::ZString &newPassword, std::string &error); + private: coreutils::ZString &mailPath; coreutils::MString queuePath; diff --git a/SMTPServer.cpp b/SMTPServer.cpp index d2f8138..f835a7c 100644 --- a/SMTPServer.cpp +++ b/SMTPServer.cpp @@ -37,6 +37,29 @@ namespace mail { out << "252 " << errorString << CRLF; } + void SMTPServer::deliverToMailbox(coreutils::ZString &mailbox, coreutils::ZString &messageId, + SpamAssassinFilter::Result &scan) { + // The destination folder has to be picked AFTER the scan runs, + // not before - this used to always build an "/Inbox/" path + // regardless of what spamd found, so a correctly-flagged spam + // message (isSpam true, logged as "(flagged as spam)" right + // below) still landed straight in Inbox every time; the Spam + // folder existed and was reachable, it just never received + // anything. scan.isSpam is only meaningful when scan.ok is + // true (spamd fails open - see SpamAssassinFilter.h's own doc + // comment - an unreachable scanner must not silently reroute + // every message to Spam). + coreutils::MString path; + path << mailFileSystem.getMailBoxPath(mailbox) + << ((scan.ok && scan.isSpam) ? "/Spam/" : "/Inbox/") << messageId; + + coreutils::File mailFile(path, O_CREAT | O_WRONLY, 0660); + mailFile.write(scan.scanned); + + coreutils::Log(coreutils::LOG_INFO) << "Message " << messageId << " delivered to recipient " << mailbox + << (scan.ok && scan.isSpam ? " (flagged as spam)" : "") << "."; + } + void SMTPServer::inCreate(coreutils::ZString &name) { coreutils::Log(coreutils::LOG_DEBUG_2) << "inCreate; [" << name << "]"; name.split("|"); @@ -45,11 +68,15 @@ namespace mail { // had to be added alongside the recipient. if(name.getList().size() > 2) { bool exists = mailFileSystem.ifMailBoxExists(name[1]); - coreutils::MString path; - path << mailFileSystem.getMailBoxPath(name[1]) << "/Inbox/" << name[0]; + // An alias is a plain file (not a directory) at the same + // level a mailbox would be - see MailFileSystem's own doc + // comment on createAlias()/ifAliasExists(). Only checked when + // it isn't a real mailbox, so a mailbox and an alias can never + // both match the same address. + bool isAlias = !exists && mailFileSystem.ifAliasExists(name[1]); coreutils::MString from; from << mailFileSystem.getQueuePath() << "/" << name; - if(exists) { + if(exists || isAlias) { // Scanned on the way into the recipient's own Inbox, not the // outbound relay path below - spamd's added X-Spam-* headers // are for whatever reads this recipient's mailbox, and a @@ -62,12 +89,18 @@ namespace mail { queuedFile.read(); SpamAssassinFilter::Result scan = SpamAssassinFilter::scan(queuedFile.asString()); - coreutils::File mailFile(path, O_CREAT | O_WRONLY, 0660); - mailFile.write(scan.scanned); - int rc = unlink(from.c_str()); + if (isAlias) { + // One shared scan result delivered to every target - an + // alias should carry one spam verdict for the message, not + // a separately re-scanned copy per target. + std::vector targets = mailFileSystem.getAliasMailboxes(name[1]); + for (auto &target : targets) + deliverToMailbox(target, name[0], scan); + } else { + deliverToMailbox(name[1], name[0], scan); + } - coreutils::Log(coreutils::LOG_INFO) << "Message " << name[0] << " delivered to recipient " << name[1] - << (scan.ok && scan.isSpam ? " (flagged as spam)" : "") << "."; + int rc = unlink(from.c_str()); } else { // Not a local mailbox - relay it out over SMTP. SendMail's // own send() is a plain blocking call (see its doc comment @@ -82,8 +115,29 @@ namespace mail { coreutils::MString mailData(queuedFile.asString()); SendMail *outbound = new SendMail(hostName, mailFileSystem.getSendLogPath().str(), name[0].str(), sender, recipient, mailData, dkimSigner); + // 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 + // arbitrarily later, or never, if the process restarts + // first) - previously this was never unlinked at all, + // meaning processExisting() re-relayed every message still + // sitting here on every single restart, sending duplicate + // copies (confirmed live: one message redelivered 35+ + // times across repeated crash-restarts). + int rc = unlink(from.c_str()); std::thread([outbound]() { - outbound->send(); + try { + outbound->send(); + } catch (...) { + // An exception escaping a detached thread's entry + // function calls std::terminate() - crashing the + // entire process, not just this one relay attempt + // (this, combined with SIGPIPE previously not being + // ignored anywhere, is what turned an occasional + // failed delivery into repeated whole-server crashes + // - see main.cpp's new signal(SIGPIPE, SIG_IGN)). + coreutils::Log(coreutils::LOG_EXCEPT) << "Outbound relay threw an exception - message not delivered."; + } delete outbound; }).detach(); } diff --git a/SMTPServer.h b/SMTPServer.h index b0ab5f7..989cd81 100644 --- a/SMTPServer.h +++ b/SMTPServer.h @@ -24,6 +24,7 @@ # include "INotify.h" # include "SendMail.h" # include "DKIMSigner.h" +# include "SpamAssassinFilter.h" namespace mail { class SMTPServer : public core::TCPServer, public core::INotify { @@ -78,6 +79,14 @@ namespace mail { void processExisting(); + // Writes one already-scanned message into one mailbox's Inbox + // or Spam folder - factored out of inCreate() so an alias + // (MailFileSystem::ifAliasExists()/getAliasMailboxes()) can + // deliver the same scanned copy to each of its target mailboxes, + // not just the single recipient a plain local delivery has. + void deliverToMailbox(coreutils::ZString &mailbox, coreutils::ZString &messageId, + SpamAssassinFilter::Result &scan); + }; } diff --git a/__ADMIN_ALIAS.cpp b/__ADMIN_ALIAS.cpp new file mode 100644 index 0000000..7ad63a9 --- /dev/null +++ b/__ADMIN_ALIAS.cpp @@ -0,0 +1,59 @@ +#include "__ADMIN_ALIAS.h" + +namespace mail { + + __ADMIN_ALIAS::__ADMIN_ALIAS(MailFileSystem &mailFileSystem) : mailFileSystem(mailFileSystem) {} + + int __ADMIN_ALIAS::processCommand(coreutils::ZString &request, core::TCPSession &session) { + if (request.getList().size() < 2) { + session.out << "Usage: alias add alias@domain target@domain[,target2@domain] | remove alias@domain | list domain" << "\r\n"; + return 1; + } + std::string verb = request[1].str(); + std::string error; + + if (verb == "add") { + if (request.getList().size() < 4) { + session.out << "Usage: alias add alias@domain target@domain[,target2@domain]" << "\r\n"; + return 1; + } + coreutils::ZString alias(request[2]); + coreutils::ZString targets(request[3]); + bool ok = mailFileSystem.createAlias(alias, targets, error); + session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n"; + } else if (verb == "remove") { + if (request.getList().size() < 3) { + session.out << "Usage: alias remove alias@domain" << "\r\n"; + return 1; + } + coreutils::ZString alias(request[2]); + bool ok = mailFileSystem.removeAlias(alias, error); + session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n"; + } else if (verb == "list") { + if (request.getList().size() < 3) { + session.out << "Usage: alias list domain" << "\r\n"; + return 1; + } + coreutils::ZString domain(request[2]); + for (auto &alias : mailFileSystem.listAliases(domain)) { + // listAliases() only returns the local part (the filename + // on disk) - getAliasMailboxes() needs the full "user@domain" + // form since it calls getMailBoxPath() itself to find that + // same file. + coreutils::MString fullAddress; + fullAddress << alias << "@" << domain; + std::string fullAddressStr = fullAddress.str(); + coreutils::ZString aliasAddress(fullAddressStr); + session.out << alias << "@" << domain << " -> "; + auto targets = mailFileSystem.getAliasMailboxes(aliasAddress); + for (size_t i = 0; i < targets.size(); ++i) + session.out << (i ? ", " : "") << targets[i]; + session.out << "\r\n"; + } + } else { + session.out << "Usage: alias add alias@domain target@domain[,target2@domain] | remove alias@domain | list domain" << "\r\n"; + } + return 1; + } + +} diff --git a/__ADMIN_ALIAS.h b/__ADMIN_ALIAS.h new file mode 100644 index 0000000..eb96f9f --- /dev/null +++ b/__ADMIN_ALIAS.h @@ -0,0 +1,27 @@ +#ifndef ____ADMIN_ALIAS_h__ +# define ____ADMIN_ALIAS_h__ + +# include "Command.h" +# include "MailFileSystem.h" + +namespace mail { + + /// + /// Admin console command: "alias add alias@domain target1@domain,target2@domain", + /// "alias remove alias@domain", "alias list domain" - see + /// MailFileSystem::createAlias()/removeAlias()/listAliases(). This is + /// also what makes aliases actually deliver mail - see + /// SMTPServer::inCreate()'s own use of ifAliasExists()/getAliasMailboxes(). + /// + + class __ADMIN_ALIAS : public core::Command { + public: + __ADMIN_ALIAS(MailFileSystem &mailFileSystem); + int processCommand(coreutils::ZString &request, core::TCPSession &session) override; + private: + MailFileSystem &mailFileSystem; + }; + +} + +#endif diff --git a/__ADMIN_DOMAIN.cpp b/__ADMIN_DOMAIN.cpp new file mode 100644 index 0000000..34179bd --- /dev/null +++ b/__ADMIN_DOMAIN.cpp @@ -0,0 +1,37 @@ +#include "__ADMIN_DOMAIN.h" + +namespace mail { + + __ADMIN_DOMAIN::__ADMIN_DOMAIN(MailFileSystem &mailFileSystem) : mailFileSystem(mailFileSystem) {} + + int __ADMIN_DOMAIN::processCommand(coreutils::ZString &request, core::TCPSession &session) { + // request was already split (by the console's own top-level + // CommandList, delimiter " ") before dispatch reached here, so + // request[0] is "domain" itself, request[1] the verb, request[2] + // the domain name (list has no third argument). + if (request.getList().size() < 2) { + session.out << "Usage: domain add|remove|list [name]" << "\r\n"; + return 1; + } + std::string verb = request[1].str(); + std::string error; + + if (verb == "add" || verb == "remove") { + if (request.getList().size() < 3) { + session.out << "Usage: domain " << verb << " " << "\r\n"; + return 1; + } + coreutils::ZString domain(request[2]); + bool ok = (verb == "add") ? mailFileSystem.createDomain(domain, error) + : mailFileSystem.removeDomain(domain, error); + session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n"; + } else if (verb == "list") { + for (auto &domain : mailFileSystem.listDomains()) + session.out << domain << "\r\n"; + } else { + session.out << "Usage: domain add|remove|list [name]" << "\r\n"; + } + return 1; + } + +} diff --git a/__ADMIN_DOMAIN.h b/__ADMIN_DOMAIN.h new file mode 100644 index 0000000..aadebab --- /dev/null +++ b/__ADMIN_DOMAIN.h @@ -0,0 +1,25 @@ +#ifndef ____ADMIN_DOMAIN_h__ +# define ____ADMIN_DOMAIN_h__ + +# include "Command.h" +# include "MailFileSystem.h" + +namespace mail { + + /// + /// Admin console command: "domain add|remove|list [name]" - see + /// MailFileSystem::createDomain()/removeDomain()/listDomains(), + /// which this just parses arguments for and reports the result of. + /// + + class __ADMIN_DOMAIN : public core::Command { + public: + __ADMIN_DOMAIN(MailFileSystem &mailFileSystem); + int processCommand(coreutils::ZString &request, core::TCPSession &session) override; + private: + MailFileSystem &mailFileSystem; + }; + +} + +#endif diff --git a/__ADMIN_MAILBOX.cpp b/__ADMIN_MAILBOX.cpp new file mode 100644 index 0000000..6af950e --- /dev/null +++ b/__ADMIN_MAILBOX.cpp @@ -0,0 +1,46 @@ +#include "__ADMIN_MAILBOX.h" + +namespace mail { + + __ADMIN_MAILBOX::__ADMIN_MAILBOX(MailFileSystem &mailFileSystem) : mailFileSystem(mailFileSystem) {} + + int __ADMIN_MAILBOX::processCommand(coreutils::ZString &request, core::TCPSession &session) { + if (request.getList().size() < 2) { + session.out << "Usage: mailbox add user@domain password | remove user@domain | list domain" << "\r\n"; + return 1; + } + std::string verb = request[1].str(); + std::string error; + + if (verb == "add") { + if (request.getList().size() < 4) { + session.out << "Usage: mailbox add user@domain password" << "\r\n"; + return 1; + } + coreutils::ZString mailbox(request[2]); + coreutils::ZString password(request[3]); + bool ok = mailFileSystem.createMailbox(mailbox, password, error); + session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n"; + } else if (verb == "remove") { + if (request.getList().size() < 3) { + session.out << "Usage: mailbox remove user@domain" << "\r\n"; + return 1; + } + coreutils::ZString mailbox(request[2]); + bool ok = mailFileSystem.removeMailbox(mailbox, error); + session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n"; + } else if (verb == "list") { + if (request.getList().size() < 3) { + session.out << "Usage: mailbox list domain" << "\r\n"; + return 1; + } + coreutils::ZString domain(request[2]); + for (auto &mailbox : mailFileSystem.listMailboxes(domain)) + session.out << mailbox << "@" << domain << "\r\n"; + } else { + session.out << "Usage: mailbox add user@domain password | remove user@domain | list domain" << "\r\n"; + } + return 1; + } + +} diff --git a/__ADMIN_MAILBOX.h b/__ADMIN_MAILBOX.h new file mode 100644 index 0000000..e084fc3 --- /dev/null +++ b/__ADMIN_MAILBOX.h @@ -0,0 +1,25 @@ +#ifndef ____ADMIN_MAILBOX_h__ +# define ____ADMIN_MAILBOX_h__ + +# include "Command.h" +# include "MailFileSystem.h" + +namespace mail { + + /// + /// Admin console command: "mailbox add user@domain password", + /// "mailbox remove user@domain", "mailbox list domain" - see + /// MailFileSystem::createMailbox()/removeMailbox()/listMailboxes(). + /// + + class __ADMIN_MAILBOX : public core::Command { + public: + __ADMIN_MAILBOX(MailFileSystem &mailFileSystem); + int processCommand(coreutils::ZString &request, core::TCPSession &session) override; + private: + MailFileSystem &mailFileSystem; + }; + +} + +#endif diff --git a/__ADMIN_PASSWORD.cpp b/__ADMIN_PASSWORD.cpp new file mode 100644 index 0000000..e1a3b12 --- /dev/null +++ b/__ADMIN_PASSWORD.cpp @@ -0,0 +1,20 @@ +#include "__ADMIN_PASSWORD.h" + +namespace mail { + + __ADMIN_PASSWORD::__ADMIN_PASSWORD(MailFileSystem &mailFileSystem) : mailFileSystem(mailFileSystem) {} + + int __ADMIN_PASSWORD::processCommand(coreutils::ZString &request, core::TCPSession &session) { + if (request.getList().size() < 4 || request[1].str() != "reset") { + session.out << "Usage: password reset user@domain newpassword" << "\r\n"; + return 1; + } + coreutils::ZString mailbox(request[2]); + coreutils::ZString newPassword(request[3]); + std::string error; + bool ok = mailFileSystem.setPassword(mailbox, newPassword, error); + session.out << (ok ? "OK." : ("Failed: " + error)) << "\r\n"; + return 1; + } + +} diff --git a/__ADMIN_PASSWORD.h b/__ADMIN_PASSWORD.h new file mode 100644 index 0000000..d89fd35 --- /dev/null +++ b/__ADMIN_PASSWORD.h @@ -0,0 +1,28 @@ +#ifndef ____ADMIN_PASSWORD_h__ +# define ____ADMIN_PASSWORD_h__ + +# include "Command.h" +# include "MailFileSystem.h" + +namespace mail { + + /// + /// Admin console command: "password reset user@domain newpassword" - + /// see MailFileSystem::setPassword(). A separate top-level command + /// (rather than a verb under "mailbox") since resetting a user's + /// password was called out as its own explicit requirement, and + /// keeping it separate leaves room for other account-level + /// configuration commands later without reworking mailbox add/remove. + /// + + class __ADMIN_PASSWORD : public core::Command { + public: + __ADMIN_PASSWORD(MailFileSystem &mailFileSystem); + int processCommand(coreutils::ZString &request, core::TCPSession &session) override; + private: + MailFileSystem &mailFileSystem; + }; + +} + +#endif diff --git a/main.cpp b/main.cpp index 4822c63..844d0f6 100644 --- a/main.cpp +++ b/main.cpp @@ -12,12 +12,27 @@ #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 @@ -135,9 +150,44 @@ int main(int argc, char **argv) { 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"); + // 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);