Files
BARANTMail/MessageHeaders.cpp
Brad Arant cd5e0a9905 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).
2026-09-03 05:24:22 +00:00

209 lines
7.2 KiB
C++

#include "MessageHeaders.h"
#include <algorithm>
#include <cctype>
namespace mail {
void splitMessage(const std::string &message, std::string &headerBlock, std::string &bodyBlock) {
size_t split = message.find("\r\n\r\n");
size_t skip = 4;
if (split == std::string::npos) {
split = message.find("\n\n");
skip = 2;
}
if (split == std::string::npos) {
headerBlock = message;
bodyBlock = "";
} else {
headerBlock = message.substr(0, split);
bodyBlock = message.substr(split + skip);
}
}
std::vector<std::pair<std::string, std::string>> parseHeaderLines(const std::string &headerBlock) {
std::vector<std::string> lines;
size_t pos = 0;
while (pos <= headerBlock.size()) {
size_t next = headerBlock.find("\r\n", pos);
size_t lineEnd = (next == std::string::npos) ? headerBlock.size() : next;
lines.push_back(headerBlock.substr(pos, lineEnd - pos));
if (next == std::string::npos)
break;
pos = next + 2;
}
std::vector<std::pair<std::string, std::string>> headers;
for (const std::string &line : lines) {
if (line.empty())
continue;
if ((line[0] == ' ' || line[0] == '\t') && !headers.empty()) {
std::string continuation = line;
size_t start = continuation.find_first_not_of(" \t");
headers.back().second += " ";
headers.back().second += (start == std::string::npos) ? "" : continuation.substr(start);
continue;
}
size_t colon = line.find(':');
if (colon == std::string::npos)
continue;
std::string name = line.substr(0, colon);
std::string value = line.substr(colon + 1);
size_t start = value.find_first_not_of(" \t");
value = (start == std::string::npos) ? "" : value.substr(start);
headers.push_back({name, value});
}
return headers;
}
std::string getHeader(const std::vector<std::pair<std::string, std::string>> &headers, const std::string &name) {
for (const auto &header : headers) {
if (header.first.size() != name.size())
continue;
bool match = true;
for (size_t i = 0; i < name.size(); ++i)
if (std::toupper((unsigned char)header.first[i]) != std::toupper((unsigned char)name[i])) {
match = false;
break;
}
if (match)
return header.second;
}
return "";
}
namespace {
std::string trim(const std::string &s) {
size_t start = s.find_first_not_of(" \t\r\n");
size_t end = s.find_last_not_of(" \t\r\n");
if (start == std::string::npos)
return "";
return s.substr(start, end - start + 1);
}
std::string stripQuotes(const std::string &s) {
std::string value = trim(s);
if (value.size() >= 2 && value.front() == '"' && value.back() == '"')
return value.substr(1, value.size() - 2);
return value;
}
Address parseOneAddress(const std::string &text) {
Address address;
size_t lt = text.find('<');
size_t gt = text.find('>');
std::string addrPart;
if (lt != std::string::npos && gt != std::string::npos && gt > lt) {
address.name = stripQuotes(trim(text.substr(0, lt)));
addrPart = text.substr(lt + 1, gt - lt - 1);
} else {
addrPart = text;
}
addrPart = trim(addrPart);
size_t at = addrPart.find_last_of('@');
if (at == std::string::npos) {
address.mailbox = addrPart;
} else {
address.mailbox = addrPart.substr(0, at);
address.host = addrPart.substr(at + 1);
}
return address;
}
}
std::vector<Address> parseAddressList(const std::string &headerValue) {
std::vector<Address> addresses;
std::string current;
bool inQuotes = false;
int angleDepth = 0;
for (char c : headerValue) {
if (c == '"')
inQuotes = !inQuotes;
else if (!inQuotes && c == '<')
++angleDepth;
else if (!inQuotes && c == '>' && angleDepth > 0)
--angleDepth;
if (c == ',' && !inQuotes && angleDepth == 0) {
std::string piece = trim(current);
if (!piece.empty())
addresses.push_back(parseOneAddress(piece));
current.clear();
continue;
}
current += c;
}
std::string piece = trim(current);
if (!piece.empty())
addresses.push_back(parseOneAddress(piece));
return addresses;
}
std::string imapQuoteOrNil(const std::string &value) {
if (value.empty())
return "NIL";
std::string escaped;
escaped.reserve(value.size() + 2);
escaped += '"';
for (char c : value) {
if (c == '"' || c == '\\')
escaped += '\\';
if (c == '\r' || c == '\n')
continue;
escaped += c;
}
escaped += '"';
return escaped;
}
namespace {
std::string addressListToEnvelope(const std::vector<Address> &addresses) {
if (addresses.empty())
return "NIL";
std::string result = "(";
for (const Address &address : addresses) {
result += "(" + imapQuoteOrNil(address.name) + " NIL " +
imapQuoteOrNil(address.mailbox) + " " + imapQuoteOrNil(address.host) + ")";
}
result += ")";
return result;
}
}
std::string buildEnvelope(const std::string &message) {
std::string headerBlock, bodyBlock;
splitMessage(message, headerBlock, bodyBlock);
auto headers = parseHeaderLines(headerBlock);
std::string date = getHeader(headers, "Date");
std::string subject = getHeader(headers, "Subject");
std::vector<Address> from = parseAddressList(getHeader(headers, "From"));
std::string senderHeader = getHeader(headers, "Sender");
std::vector<Address> sender = senderHeader.empty() ? from : parseAddressList(senderHeader);
std::string replyToHeader = getHeader(headers, "Reply-To");
std::vector<Address> replyTo = replyToHeader.empty() ? from : parseAddressList(replyToHeader);
std::vector<Address> to = parseAddressList(getHeader(headers, "To"));
std::vector<Address> cc = parseAddressList(getHeader(headers, "Cc"));
std::vector<Address> bcc = parseAddressList(getHeader(headers, "Bcc"));
std::string inReplyTo = getHeader(headers, "In-Reply-To");
std::string messageId = getHeader(headers, "Message-ID");
std::string envelope = "(";
envelope += imapQuoteOrNil(date) + " ";
envelope += imapQuoteOrNil(subject) + " ";
envelope += addressListToEnvelope(from) + " ";
envelope += addressListToEnvelope(sender) + " ";
envelope += addressListToEnvelope(replyTo) + " ";
envelope += addressListToEnvelope(to) + " ";
envelope += addressListToEnvelope(cc) + " ";
envelope += addressListToEnvelope(bcc) + " ";
envelope += imapQuoteOrNil(inReplyTo) + " ";
envelope += imapQuoteOrNil(messageId);
envelope += ")";
return envelope;
}
}