Files
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

126 lines
4.8 KiB
C++

#ifndef __IMAPArgs_h__
#define __IMAPArgs_h__
#include "ZString.h"
#include <cstdint>
#include <ctime>
#include <string>
#include <vector>
namespace mail {
class MailFolder;
///
/// Shared argument parsing for every IMAP command handler.
///
/// CommandList::processRequest() (ServerCore) pre-splits a command
/// line on literal spaces before any handler sees it - fine for SMTP,
/// wrong for IMAP, whose arguments routinely contain quoted strings
/// and parenthesized lists with embedded spaces
/// (LOGIN "user name" pw, STORE 1 +FLAGS (\Seen \Flagged)). Handlers
/// must never read request[N]; instead they call nextArgument()
/// repeatedly against the same ZString&, which parses off the RAW,
/// unsplit cursor (request.reset(), already done by
/// CommandList::processRequest() before dispatch, put the cursor back
/// at the real start).
///
namespace IMAPArgs {
///
/// Skips leading whitespace, then returns the next IMAP argument:
/// an atom (run up to the next space), a quoted "..." string, or a
/// (...)/[...]/{...} container - all via ZString::getContainer(),
/// which already understands these four delimiter shapes,
/// INCLUDING their delimiters. Callers that want a quoted string's
/// interior, or a container's interior, strip the surrounding
/// characters themselves (different callers want different
/// things: FETCH wants to know it got a "[...]" section, STORE just
/// wants a plain flag list). Returns an empty ZString at end of
/// input.
///
coreutils::ZString nextArgument(coreutils::ZString &request);
///
/// Case-insensitive compare of an already-extracted argument
/// against a bare keyword (FLAGS, SEEN, BEFORE, ...).
///
bool isKeyword(coreutils::ZString &argument, const char *keyword);
///
/// Strips a single layer of "..." or (...) or [...] delimiters
/// from an argument previously returned by nextArgument(), if
/// present. Leaves an atom (no delimiters) unchanged.
///
std::string unwrap(coreutils::ZString &argument);
///
/// Parses an IMAP sequence-set ("1:5,7,9:*") against folder's
/// CURRENT message ordering. uidMode=true: the numbers in the set
/// are UIDs and "*" is the highest UID present; uidMode=false:
/// numbers are 1-based sequence numbers and "*" is the last
/// message. Numbers outside the folder's current bounds are
/// silently skipped (matches real server behavior - a client can
/// race a concurrent EXPUNGE). Returns the resolved UIDs, ascending
/// and de-duplicated.
///
std::vector<uint32_t> parseSequenceSet(coreutils::ZString &request, MailFolder &folder, bool uidMode);
///
/// "(\Seen \Flagged)" -> {"\Seen", "\Flagged"}. Also accepts a
/// bare unparenthesized single flag, for STORE clients that omit
/// the parens around a single-flag list.
///
std::vector<std::string> parseFlagList(coreutils::ZString &request);
///
/// RFC 3501 date-time ("DD-Mon-YYYY HH:MM:SS +ZZZZ", always
/// quoted - APPEND's optional date-time argument) or bare date
/// ("DD-Mon-YYYY", optionally quoted - SEARCH's BEFORE/ON/SINCE) ->
/// Unix timestamp. Returns -1 on unparseable input.
///
time_t parseDate(coreutils::ZString &argument);
///
/// RFC 3501 5.1: the mailbox name INBOX is reserved and MUST be
/// interpreted case-insensitively, referring to the user's
/// primary mailbox - every other mailbox name is server-defined
/// and compared literally. On-disk folders are named "Inbox",
/// "Spam", "Trash" (capitalized) - this maps any-case "inbox" to
/// that literal directory name and passes every other name
/// through unchanged.
///
std::string normalizeFolderName(const std::string &name);
///
/// Every folder-taking command turns a client-supplied name
/// straight into "<mailbox root>/<name>" (MailFileSystem::
/// getFolderPath()). A name containing '/' escapes into a
/// subdirectory; a bare ".." - no slash, so a check for '/' alone
/// does NOT catch it - resolves to the mailbox's own PARENT
/// directory, which really exists and really is a directory, so
/// ifFolderExists() would return true for it: SELECT/DELETE/
/// APPEND/RENAME would then operate on the directory holding
/// every OTHER user's mailbox on the same domain. Rejects empty,
/// anything containing '/', and anything starting with '.'
/// (which also reserves the leading-dot namespace for this
/// server's own index files - .password, .uidvalidity, .uidnext,
/// .imapindex, .subscriptions).
///
bool isValidFolderName(const std::string &name);
}
}
#endif