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).
155 lines
5.6 KiB
C++
155 lines
5.6 KiB
C++
#include "IMAPArgs.h"
|
|
#include "MailFolder.h"
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <sstream>
|
|
|
|
namespace mail {
|
|
|
|
namespace IMAPArgs {
|
|
|
|
coreutils::ZString nextArgument(coreutils::ZString &request) {
|
|
request.skipWhitespace();
|
|
if (request.eod())
|
|
return coreutils::ZString();
|
|
char c = request.getChar();
|
|
if (c == '"' || c == '(' || c == '[' || c == '{')
|
|
return request.getContainer();
|
|
return request.getTokenExclude(" ");
|
|
}
|
|
|
|
bool isKeyword(coreutils::ZString &argument, const char *keyword) {
|
|
std::string a = argument.str();
|
|
std::string b = keyword;
|
|
if (a.size() != b.size())
|
|
return false;
|
|
for (size_t i = 0; i < a.size(); ++i)
|
|
if (std::toupper((unsigned char)a[i]) != std::toupper((unsigned char)b[i]))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
std::string unwrap(coreutils::ZString &argument) {
|
|
std::string value = argument.str();
|
|
if (value.size() >= 2) {
|
|
char first = value.front();
|
|
char last = value.back();
|
|
if ((first == '"' && last == '"') ||
|
|
(first == '(' && last == ')') ||
|
|
(first == '[' && last == ']') ||
|
|
(first == '{' && last == '}'))
|
|
return value.substr(1, value.size() - 2);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
std::vector<uint32_t> parseSequenceSet(coreutils::ZString &request, MailFolder &folder, bool uidMode) {
|
|
std::vector<uint32_t> result;
|
|
|
|
// Always resolves to a concrete UID, regardless of uidMode -
|
|
// callers below (including the range-walking code, which
|
|
// re-derives sequence numbers from these UIDs via folder.seq())
|
|
// depend on that.
|
|
auto resolveNumber = [&](const std::string &token) -> uint32_t {
|
|
if (token == "*")
|
|
return folder.size() == 0 ? 0 : folder.messages().back().uid;
|
|
uint32_t value = (uint32_t)std::strtoul(token.c_str(), nullptr, 10);
|
|
if (uidMode)
|
|
return folder.find(value) ? value : 0;
|
|
return folder.uidForSeq(value);
|
|
};
|
|
|
|
coreutils::ZString set = nextArgument(request);
|
|
std::string setStr = set.str();
|
|
std::istringstream stream(setStr);
|
|
std::string part;
|
|
while (std::getline(stream, part, ',')) {
|
|
if (part.empty())
|
|
continue;
|
|
size_t colon = part.find(':');
|
|
if (colon == std::string::npos) {
|
|
uint32_t uid = resolveNumber(part);
|
|
if (uid != 0)
|
|
result.push_back(uid);
|
|
} else {
|
|
std::string fromStr = part.substr(0, colon);
|
|
std::string toStr = part.substr(colon + 1);
|
|
uint32_t fromUid = resolveNumber(fromStr);
|
|
uint32_t toUid = resolveNumber(toStr);
|
|
if (fromUid == 0 || toUid == 0)
|
|
continue;
|
|
if (fromUid > toUid)
|
|
std::swap(fromUid, toUid);
|
|
// resolveNumber() already turned both ends into concrete
|
|
// UIDs regardless of uidMode - walking sequence-number-
|
|
// by-sequence-number (not uid-by-uid) from here is
|
|
// deliberate: a UID range like "5:9" must include every
|
|
// message whose UID falls in that range, not just
|
|
// messages whose UID happens to equal one of the
|
|
// integers 5-9 (UIDs aren't dense/contiguous once
|
|
// anything's been expunged).
|
|
uint32_t fromSeq = folder.seq(fromUid);
|
|
uint32_t toSeq = folder.seq(toUid);
|
|
if (fromSeq == 0 || toSeq == 0)
|
|
continue;
|
|
for (uint32_t s = fromSeq; s <= toSeq; ++s)
|
|
result.push_back(folder.uidForSeq(s));
|
|
}
|
|
}
|
|
|
|
std::sort(result.begin(), result.end());
|
|
result.erase(std::unique(result.begin(), result.end()), result.end());
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::string> parseFlagList(coreutils::ZString &request) {
|
|
coreutils::ZString arg = nextArgument(request);
|
|
std::string text = unwrap(arg);
|
|
std::vector<std::string> flags;
|
|
std::istringstream stream(text);
|
|
std::string token;
|
|
while (stream >> token)
|
|
flags.push_back(token);
|
|
return flags;
|
|
}
|
|
|
|
time_t parseDate(coreutils::ZString &argument) {
|
|
std::string text = unwrap(argument);
|
|
struct tm tmValue = {};
|
|
// Try the full APPEND date-time form first, then fall back to
|
|
// SEARCH's bare date form.
|
|
if (strptime(text.c_str(), "%d-%b-%Y %H:%M:%S %z", &tmValue) != nullptr)
|
|
return timegm(&tmValue) - tmValue.tm_gmtoff;
|
|
tmValue = {};
|
|
if (strptime(text.c_str(), "%d-%b-%Y", &tmValue) != nullptr)
|
|
return timegm(&tmValue);
|
|
return (time_t)-1;
|
|
}
|
|
|
|
bool isValidFolderName(const std::string &name) {
|
|
if (name.empty())
|
|
return false;
|
|
if (name.find('/') != std::string::npos)
|
|
return false;
|
|
if (name[0] == '.')
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
std::string normalizeFolderName(const std::string &name) {
|
|
if (name.size() == 5) {
|
|
std::string upper = name;
|
|
for (char &c : upper)
|
|
c = std::toupper((unsigned char)c);
|
|
if (upper == "INBOX")
|
|
return "Inbox";
|
|
}
|
|
return name;
|
|
}
|
|
|
|
}
|
|
|
|
}
|