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).
126 lines
4.8 KiB
C++
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
|