#include "MailFileSystem.h" # include "INotify.h" # include "Log.h" # include "File.h" # include # include namespace mail { MailFileSystem::MailFileSystem(coreutils::ZString &mailPath) : mailPath(mailPath) { queuePath << mailPath << "/.queue/"; sendLogPath << mailPath << "/.sendlog/"; // Harmless if it already exists (EEXIST is ignored, same as // every other error here - a failed mkdir just means the later // File open in SendMail::deliver() throws instead, which does // not affect whether the message itself is delivered). mkdir(sendLogPath.c_str(), 0770); } bool MailFileSystem::ifMailBoxExists(coreutils::ZString &mailbox) { coreutils::Log(coreutils::LOG_DEBUG_2) << "if exists mailbox: " << mailbox << "."; if(stat(getMailBoxPath(mailbox).c_str(), &statbuf) != -1) if(S_ISDIR(statbuf.st_mode)) return true; coreutils::Log(coreutils::LOG_DEBUG_2) << "doesnt exist mailbox: " << mailbox << "."; return false; } bool MailFileSystem::ifAliasExists(coreutils::ZString &alias) { if(stat(getMailBoxPath(alias).c_str(), &statbuf) != -1) if(S_ISREG(statbuf.st_mode)) return true; return false; } coreutils::MString MailFileSystem::getMailBoxPath(coreutils::ZString &mailbox) { mailbox.split("@", 1); // A mailbox string with no "@" splits to a single-element list - // mailbox[1] would then be an out-of-bounds std::vector access // (ZString::operator[] doesn't bounds-check), not even a // catchable exception. Previously unreachable with a safe input // in practice; now reachable directly from a client-supplied IMAP // LOGIN/POP3 USER username, so a malformed one (a bare "barant" // instead of "barant@barant.com", an easy real typo) needs to // fail cleanly here rather than read adjacent memory. if (mailbox.getList().size() < 2) return coreutils::MString(); coreutils::MString path; path << mailPath << "/" << mailbox[1] << "/" << mailbox[0]; return path; } 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; } coreutils::ZString& MailFileSystem::getMailPath() { return mailPath; } coreutils::ZString& MailFileSystem::getQueuePath() { return queuePath; } coreutils::ZString& MailFileSystem::getSendLogPath() { return sendLogPath; } bool MailFileSystem::checkPassword(coreutils::ZString &userName, coreutils::ZString &password) { // File's constructor throws if the mailbox or its .password file // doesn't exist - an unknown username needs to come back as "not // authenticated", not an uncaught exception. Previously (as // __SMTP_AUTH::authLogin(), before this logic moved here) this // was only saved from crashing the whole process by // CommandList::processRequest()'s own unrelated catch-all, which // treated the exception as "could not be parsed" and silently // dropped the reply entirely - a client attempting AUTH LOGIN (or // now IMAP LOGIN, POP3 USER/PASS) against a nonexistent user got // no response at all rather than a clean authentication failure. try { coreutils::MString secretPath; secretPath << getMailBoxPath(userName) << "/.password"; coreutils::File secret(secretPath); secret.read(); return password == secret.asZString(); } catch (...) { return false; } } coreutils::MString MailFileSystem::getFolderPath(coreutils::ZString &mailbox, std::string folder) { coreutils::MString path; path << getMailBoxPath(mailbox) << "/" << folder; return path; } bool MailFileSystem::ifFolderExists(coreutils::ZString &mailbox, std::string folder) { struct stat folderStat; return stat(getFolderPath(mailbox, folder).c_str(), &folderStat) != -1 && S_ISDIR(folderStat.st_mode); } std::vector MailFileSystem::listFolders(coreutils::ZString &mailbox) { std::vector folders; std::string root = getMailBoxPath(mailbox).str(); if (!std::filesystem::is_directory(root)) return folders; for (const auto &entry : std::filesystem::directory_iterator(root)) { std::string name = entry.path().filename().string(); if (!name.empty() && name[0] != '.' && entry.is_directory()) folders.push_back(coreutils::MString(name)); } 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; } }