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).
This commit is contained in:
Brad Arant committed 2026-09-03 05:24:22 +00:00
1 parent 641368ea85
commit cd5e0a9905
122 files changed
+6059 -570

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
#include "BARANTMailConfig.h"
#include "Exception.h"
#include "JFile.h"
namespace mail {
BARANTMailConfig::BARANTMailConfig(const std::string &path) {
file = new coreutils::JFile(coreutils::ZString(path.c_str(), path.length()));
if (!file->exists("hostName"))
throw coreutils::Exception("BARANTMailConfig: \"hostName\" is missing from " + path);
if (!file->exists("mailPath"))
throw coreutils::Exception("BARANTMailConfig: \"mailPath\" is missing from " + path);
if (!file->exists("smtp.port"))
throw coreutils::Exception("BARANTMailConfig: \"smtp.port\" is missing from " + path);
bool needsTls = file->exists("smtpTLS.port") || file->exists("imap.port") || file->exists("pop3.port");
if (needsTls) {
if (!file->exists("tls.certificate") || !file->exists("tls.key"))
throw coreutils::Exception(
"BARANTMailConfig: smtpTLS/imap/pop3 is configured but \"tls.certificate\"/\"tls.key\" "
"is missing from " + path);
certPath = file->getString("tls.certificate");
keyPath = file->getString("tls.key");
tlsInfo.certificate = certPath.c_str();
tlsInfo.key = keyPath.c_str();
if (file->exists("tls.caCertificate")) {
caPath = file->getString("tls.caCertificate");
tlsInfo.cACertificate = caPath.c_str();
}
hasTls = true;
}
}
BARANTMailConfig::~BARANTMailConfig() {
delete file;
}
std::string BARANTMailConfig::getHostName() {
return file->getString("hostName");
}
std::string BARANTMailConfig::getMailPath() {
return file->getString("mailPath");
}
int BARANTMailConfig::getSmtpPort() {
return (int)file->getInt("smtp.port");
}
bool BARANTMailConfig::hasSmtpTLS() {
return file->exists("smtpTLS.port");
}
int BARANTMailConfig::getSmtpTLSPort() {
return (int)file->getInt("smtpTLS.port");
}
bool BARANTMailConfig::hasImap() {
return file->exists("imap.port");
}
int BARANTMailConfig::getImapPort() {
return (int)file->getInt("imap.port");
}
bool BARANTMailConfig::hasPop3() {
return file->exists("pop3.port");
}
int BARANTMailConfig::getPop3Port() {
return (int)file->getInt("pop3.port");
}
core::TLSInfo *BARANTMailConfig::getTlsInfo() {
return hasTls ? &tlsInfo : NULL;
}
bool BARANTMailConfig::hasDkim() {
return file->exists("dkim.domain") && file->exists("dkim.selector") && file->exists("dkim.privateKey");
}
std::string BARANTMailConfig::getDkimDomain() {
return file->getString("dkim.domain");
}
std::string BARANTMailConfig::getDkimSelector() {
return file->getString("dkim.selector");
}
std::string BARANTMailConfig::getDkimPrivateKeyPath() {
return file->getString("dkim.privateKey");
}
}
+104
View File
@@ -0,0 +1,104 @@
#ifndef __BARANTMailConfig_h__
#define __BARANTMailConfig_h__
#include "TLSInfo.h"
#include <string>
namespace coreutils { class JFile; }
namespace mail {
///
/// Server startup configuration, loaded from a JSON file via
/// CoreUtils' JFile - mirrors APIServer's own ApiServerConfig (see
/// that class's doc comment for JFile's create-on-missing behavior).
///
/// Schema:
/// {
/// "hostName": "mail.example.com",
/// "mailPath": "/var/mail",
/// "smtp": { "port": 25 },
/// "smtpTLS": { "port": 465 },
/// "imap": { "port": 993 },
/// "pop3": { "port": 995 },
/// "tls": {
/// "certificate": "/path/to/fullchain.pem",
/// "key": "/path/to/privkey.pem",
/// "caCertificate": "/path/to/ca.pem"
/// },
/// "dkim": {
/// "domain": "mail.example.com",
/// "selector": "default",
/// "privateKey": "/path/to/dkim_private.pem"
/// }
/// }
///
/// "hostName", "mailPath", and "smtp.port" are required - plain port
/// 25 is the one listener every deployment needs (an inbound peer
/// MTA relaying mail in has no TLS-first negotiation to fall back
/// on). "smtpTLS"/"imap"/"pop3" are each independently optional -
/// leaving one out just means that listener isn't started, so this
/// same file covers a plaintext-only relay just as well as a full
/// mail server. The moment any one of those three IS present,
/// "tls.certificate" and "tls.key" become required - there is no
/// path through this class that hands back a submission/IMAPS/POP3S
/// listener request without a real certificate configured for it.
/// "dkim" is entirely optional - outbound mail goes out unsigned
/// unless all three of its fields are present.
///
class BARANTMailConfig {
public:
BARANTMailConfig(const std::string &path);
~BARANTMailConfig();
std::string getHostName();
std::string getMailPath();
int getSmtpPort();
bool hasSmtpTLS();
int getSmtpTLSPort();
bool hasImap();
int getImapPort();
bool hasPop3();
int getPop3Port();
///
/// NULL if none of smtpTLS/imap/pop3 are configured. Owned by
/// this BARANTMailConfig, valid for as long as it is - pass
/// straight through to a server's tlsInfo parameter, do not
/// delete, and don't let this config object go out of scope
/// while a server built from this pointer is still running.
///
core::TLSInfo *getTlsInfo();
bool hasDkim();
std::string getDkimDomain();
std::string getDkimSelector();
std::string getDkimPrivateKeyPath();
private:
coreutils::JFile *file;
// Owned, persistent storage for the TLSInfo fields below -
// TLSInfo's own ZString members are non-owning views, so they
// have to point at something that outlives them; pointing them at
// temporary std::strings decoded during construction would leave
// them dangling the moment this constructor returned.
std::string certPath, keyPath, caPath;
core::TLSInfo tlsInfo;
bool hasTls = false;
};
}
#endif
BIN
View File
Binary file not shown.
+265
View File
@@ -0,0 +1,265 @@
#include "DKIMSigner.h"
#include <cctype>
#include <cstring>
#include <ctime>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <sstream>
#include <stdexcept>
namespace mail {
DKIMSigner::DKIMSigner(std::string domain, std::string selector, std::string privateKeyPath)
: domain(domain), selector(selector), privateKey(nullptr) {
FILE *fp = fopen(privateKeyPath.c_str(), "r");
if (!fp)
throw std::runtime_error("DKIMSigner: could not open private key " + privateKeyPath);
privateKey = PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr);
fclose(fp);
if (!privateKey)
throw std::runtime_error("DKIMSigner: could not parse private key " + privateKeyPath);
}
DKIMSigner::~DKIMSigner() {
if (privateKey)
EVP_PKEY_free(privateKey);
}
// Reduces every run of SP/TAB to a single SP, then trims leading and
// trailing SP entirely - the relaxed canonicalization rule for
// whitespace *around* a header field's name/value (RFC 6376 3.4.2),
// distinct from the body's own version of this rule (canonicalizeBody),
// which reduces internal runs the same way but does NOT strip a
// leading run down to nothing, only to one space.
std::string DKIMSigner::collapseWSP(const std::string &s) {
std::string result;
bool lastWasSpace = false;
for (char c : s) {
if (c == ' ' || c == '\t') {
if (!lastWasSpace)
result += ' ';
lastWasSpace = true;
} else {
result += c;
lastWasSpace = false;
}
}
size_t start = 0;
while (start < result.size() && result[start] == ' ')
++start;
size_t end = result.size();
while (end > start && result[end - 1] == ' ')
--end;
return result.substr(start, end - start);
}
// RFC 6376 3.4.2 (relaxed header canonicalization), applied to one
// already-unfolded (name, value) pair: lowercase the name, delete
// WSP around the colon, collapse+trim WSP within the value, one
// trailing CRLF.
std::string DKIMSigner::canonicalizeHeaderField(const std::string &name, const std::string &value) {
std::string lowerName = name;
for (char &c : lowerName)
c = std::tolower((unsigned char)c);
return lowerName + ":" + collapseWSP(value) + "\r\n";
}
// RFC 6376 3.4.4 (relaxed body canonicalization): collapse WSP runs
// within each line to a single SP (a leading run becomes one SP, not
// zero - unlike header canonicalization above), strip trailing WSP
// from each line, then drop trailing empty lines so the body ends in
// exactly one CRLF (or, if it was empty/all-whitespace to begin
// with, canonicalizes to a single CRLF - RFC 6376 3.4.3's explicit
// "null body" case).
std::string DKIMSigner::canonicalizeBody(const std::string &body) {
std::vector<std::string> lines;
size_t pos = 0;
while (pos < body.size()) {
size_t next = body.find("\r\n", pos);
if (next == std::string::npos) {
lines.push_back(body.substr(pos));
break;
}
lines.push_back(body.substr(pos, next - pos));
pos = next + 2;
}
for (auto &line : lines) {
std::string collapsed;
bool lastWasSpace = false;
for (char c : line) {
if (c == ' ' || c == '\t') {
if (!lastWasSpace)
collapsed += ' ';
lastWasSpace = true;
} else {
collapsed += c;
lastWasSpace = false;
}
}
while (!collapsed.empty() && collapsed.back() == ' ')
collapsed.pop_back();
line = collapsed;
}
while (!lines.empty() && lines.back().empty())
lines.pop_back();
std::string result;
for (auto &line : lines) {
result += line;
result += "\r\n";
}
if (result.empty())
result = "\r\n";
return result;
}
std::vector<DKIMSigner::HeaderField> DKIMSigner::parseHeaders(const std::string &message) {
std::vector<HeaderField> fields;
size_t headerEnd = message.find("\r\n\r\n");
std::string headerBlock = (headerEnd == std::string::npos) ? message : message.substr(0, headerEnd);
std::vector<std::string> rawLines;
size_t pos = 0;
while (pos < headerBlock.size()) {
size_t next = headerBlock.find("\r\n", pos);
std::string line = (next == std::string::npos) ? headerBlock.substr(pos) : headerBlock.substr(pos, next - pos);
rawLines.push_back(line);
if (next == std::string::npos)
break;
pos = next + 2;
}
// RFC 5322 3.2.2 folding: a continuation line starts with SP/TAB
// and belongs to the previous header field - unfolded here by
// joining with a single space, matching what canonicalizeHeaderField's
// own WSP collapsing expects to receive.
for (auto &line : rawLines) {
if (line.empty())
continue;
if ((line[0] == ' ' || line[0] == '\t') && !fields.empty()) {
fields.back().value += " " + line;
continue;
}
size_t colon = line.find(':');
if (colon == std::string::npos)
continue;
HeaderField f;
f.name = line.substr(0, colon);
f.value = line.substr(colon + 1);
fields.push_back(f);
}
return fields;
}
std::string DKIMSigner::bodyOf(const std::string &message) {
size_t headerEnd = message.find("\r\n\r\n");
if (headerEnd == std::string::npos)
return "";
return message.substr(headerEnd + 4);
}
std::string DKIMSigner::base64(const std::string &raw) {
// BIO-based rather than reusing coreutils::Base64 here deliberately
// - this file only needs OpenSSL's own headers (already required
// for EVP signing), so encoding through the same library avoids
// pulling CoreUtils' ZString/MString machinery into a class whose
// only job is talking to libcrypto.
BIO *b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
BIO *mem = BIO_new(BIO_s_mem());
BIO_push(b64, mem);
BIO_write(b64, raw.data(), (int)raw.size());
BIO_flush(b64);
char *encodedData = nullptr;
long encodedLen = BIO_get_mem_data(mem, &encodedData);
std::string result(encodedData, encodedLen);
BIO_free_all(b64);
return result;
}
std::string DKIMSigner::sha256Base64(const std::string &data) {
unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int hashLen = 0;
EVP_Digest(data.data(), data.size(), hash, &hashLen, EVP_sha256(), nullptr);
return base64(std::string((char *)hash, hashLen));
}
std::string DKIMSigner::rsaSha256SignBase64(const std::string &data) {
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
std::string result;
if (EVP_DigestSignInit(mdctx, nullptr, EVP_sha256(), nullptr, privateKey) == 1 &&
EVP_DigestSignUpdate(mdctx, data.data(), data.size()) == 1) {
size_t sigLen = 0;
EVP_DigestSignFinal(mdctx, nullptr, &sigLen);
std::vector<unsigned char> sig(sigLen);
if (EVP_DigestSignFinal(mdctx, sig.data(), &sigLen) == 1)
result = base64(std::string((char *)sig.data(), sigLen));
}
EVP_MD_CTX_free(mdctx);
if (result.empty())
throw std::runtime_error("DKIMSigner: RSA-SHA256 signing failed");
return result;
}
std::string DKIMSigner::sign(const std::string &message) {
std::vector<HeaderField> headers = parseHeaders(message);
// Sign whichever of these standard headers the message actually
// has - From is the one DMARC alignment actually depends on, the
// rest are along for integrity (a receiver seeing a signed
// Subject/Date/Message-ID that doesn't match the visible one is
// exactly the tampering DKIM exists to catch). Deliberately not
// signing headers this relay itself can still legitimately need
// to touch or add downstream of this point.
static const char *toSign[] = {"From", "To", "Subject", "Date", "Message-ID"};
std::string signedHeaderNames;
std::string headerCanon;
bool first = true;
for (const char *wanted : toSign) {
for (auto &f : headers) {
bool matches = f.name.size() == strlen(wanted);
if (matches)
for (size_t i = 0; i < f.name.size() && matches; ++i)
matches = std::tolower((unsigned char)f.name[i]) == std::tolower((unsigned char)wanted[i]);
if (matches) {
headerCanon += canonicalizeHeaderField(f.name, f.value);
if (!first)
signedHeaderNames += ":";
signedHeaderNames += wanted;
first = false;
break;
}
}
}
std::string bodyHash = sha256Base64(canonicalizeBody(bodyOf(message)));
std::stringstream tagsNoSig;
tagsNoSig << "v=1; a=rsa-sha256; c=relaxed/relaxed; d=" << domain << "; s=" << selector
<< "; t=" << (long)time(nullptr) << "; h=" << signedHeaderNames
<< "; bh=" << bodyHash << "; b=";
// The signature covers the selected headers (in the order signed
// above) followed by this DKIM-Signature field itself, with an
// empty b= - unlike every other signed header, this last one is
// included WITHOUT a trailing CRLF (RFC 6376 3.7): the field
// being created doesn't have one yet, since nothing follows it
// until sign() returns.
std::string signingInput = headerCanon + canonicalizeHeaderField("DKIM-Signature", tagsNoSig.str());
signingInput.erase(signingInput.size() - 2); // drop the trailing CRLF just for this last field
std::string signature = rsaSha256SignBase64(signingInput);
return "DKIM-Signature: " + tagsNoSig.str() + signature + "\r\n";
}
}
+64
View File
@@ -0,0 +1,64 @@
#ifndef __DKIMSigner_h__
#define __DKIMSigner_h__
#include <openssl/evp.h>
#include <string>
#include <vector>
namespace mail {
///
/// DKIMSigner produces a DKIM-Signature header (RFC 6376) for an
/// outbound message, using the relaxed/relaxed canonicalization and
/// rsa-sha256, the combination essentially every real deployment
/// uses (relaxed tolerates the whitespace/line-ending massaging a
/// message can pick up in transit; simple does not, and breaks
/// easily as a result).
///
/// One DKIMSigner is built for one (domain, selector, private key)
/// triple and can sign any number of messages with it - construction
/// loads and parses the private key once; sign() itself does no I/O.
///
class DKIMSigner {
public:
DKIMSigner(std::string domain, std::string selector, std::string privateKeyPath);
~DKIMSigner();
///
/// message is the complete outbound message exactly as it will be
/// sent in the DATA phase - CRLF-terminated header lines, a blank
/// line, then the body. Returns a complete "DKIM-Signature:
/// ...\r\n" line, ready to prepend to `message` as its new first
/// header.
///
std::string sign(const std::string &message);
private:
struct HeaderField {
std::string name; // original case, as it appeared
std::string value; // unfolded (continuation lines joined), not yet whitespace-collapsed
};
std::vector<HeaderField> parseHeaders(const std::string &message);
std::string bodyOf(const std::string &message);
std::string collapseWSP(const std::string &s);
std::string canonicalizeHeaderField(const std::string &name, const std::string &value);
std::string canonicalizeBody(const std::string &body);
std::string base64(const std::string &raw);
std::string sha256Base64(const std::string &data);
std::string rsaSha256SignBase64(const std::string &data);
std::string domain;
std::string selector;
EVP_PKEY *privateKey;
};
}
#endif
+405
View File
@@ -0,0 +1,405 @@
#include "DKIMVerifier.h"
#include <arpa/nameser.h>
#include <cctype>
#include <cstring>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/x509.h>
#include <resolv.h>
#include <sstream>
namespace mail {
namespace {
struct RawHeader {
std::string name; // original case
std::string rawText; // exact bytes as they appeared, folding included, ending in CRLF
std::string unfoldedValue; // continuation lines joined with a single space, not yet WSP-collapsed
};
std::vector<RawHeader> parseRawHeaders(const std::string &message) {
std::vector<RawHeader> headers;
size_t headerEnd = message.find("\r\n\r\n");
std::string block = (headerEnd == std::string::npos) ? message : message.substr(0, headerEnd);
std::vector<std::string> rawLines;
size_t pos = 0;
while (pos < block.size()) {
size_t next = block.find("\r\n", pos);
std::string line = (next == std::string::npos) ? block.substr(pos) : block.substr(pos, next - pos);
rawLines.push_back(line);
if (next == std::string::npos)
break;
pos = next + 2;
}
for (auto &line : rawLines) {
if (line.empty())
continue;
if ((line[0] == ' ' || line[0] == '\t') && !headers.empty()) {
headers.back().rawText += line + "\r\n";
headers.back().unfoldedValue += " " + line;
continue;
}
size_t colon = line.find(':');
if (colon == std::string::npos)
continue;
RawHeader h;
h.name = line.substr(0, colon);
h.rawText = line + "\r\n";
h.unfoldedValue = line.substr(colon + 1);
headers.push_back(h);
}
return headers;
}
std::string bodyOf(const std::string &message) {
size_t headerEnd = message.find("\r\n\r\n");
if (headerEnd == std::string::npos)
return "";
return message.substr(headerEnd + 4);
}
std::string collapseWSP(const std::string &s) {
std::string result;
bool lastWasSpace = false;
for (char c : s) {
if (c == ' ' || c == '\t') {
if (!lastWasSpace)
result += ' ';
lastWasSpace = true;
} else {
result += c;
lastWasSpace = false;
}
}
size_t start = 0;
while (start < result.size() && result[start] == ' ')
++start;
size_t end = result.size();
while (end > start && result[end - 1] == ' ')
--end;
return result.substr(start, end - start);
}
// RFC 6376 3.4.2 (relaxed) - as in DKIMSigner. Returns rawText
// unmodified for 3.4.1 (simple), which requires the header field
// exactly as it appeared, folding included.
std::string canonicalizeHeaderField(bool simple, const RawHeader &h) {
if (simple)
return h.rawText;
std::string lowerName = h.name;
for (char &c : lowerName)
c = std::tolower((unsigned char)c);
return lowerName + ":" + collapseWSP(h.unfoldedValue) + "\r\n";
}
// For synthesizing the DKIM-Signature field itself (with b=
// emptied) under simple canonicalization, which needs the same
// "exactly as it appeared" rule applied to a header this code
// built rather than one that was actually received on the wire -
// so "exactly as it appeared" here means exactly the tag text,
// with the SAME folding (none) it already has.
std::string canonicalizeSyntheticHeader(bool simple, const std::string &name, const std::string &value) {
if (simple)
return name + ":" + value + "\r\n";
std::string lowerName = name;
for (char &c : lowerName)
c = std::tolower((unsigned char)c);
return lowerName + ":" + collapseWSP(value) + "\r\n";
}
std::string canonicalizeBody(bool simple, const std::string &body) {
std::vector<std::string> lines;
size_t pos = 0;
while (pos < body.size()) {
size_t next = body.find("\r\n", pos);
if (next == std::string::npos) {
lines.push_back(body.substr(pos));
break;
}
lines.push_back(body.substr(pos, next - pos));
pos = next + 2;
}
if (!simple) {
for (auto &line : lines) {
std::string collapsed;
bool lastWasSpace = false;
for (char c : line) {
if (c == ' ' || c == '\t') {
if (!lastWasSpace)
collapsed += ' ';
lastWasSpace = true;
} else {
collapsed += c;
lastWasSpace = false;
}
}
while (!collapsed.empty() && collapsed.back() == ' ')
collapsed.pop_back();
line = collapsed;
}
}
while (!lines.empty() && lines.back().empty())
lines.pop_back();
std::string result;
for (auto &line : lines) {
result += line;
result += "\r\n";
}
if (result.empty())
result = "\r\n";
return result;
}
std::string base64Decode(const std::string &encoded) {
BIO *b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
BIO *mem = BIO_new_mem_buf(encoded.data(), (int)encoded.size());
BIO_push(b64, mem);
std::string out(encoded.size(), '\0');
int len = BIO_read(b64, out.data(), (int)out.size());
BIO_free_all(b64);
if (len <= 0)
return "";
out.resize(len);
return out;
}
std::string sha1or256Base64(bool useSha1, const std::string &data) {
unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int hashLen = 0;
EVP_Digest(data.data(), data.size(), hash, &hashLen, useSha1 ? EVP_sha1() : EVP_sha256(), nullptr);
BIO *b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
BIO *mem = BIO_new(BIO_s_mem());
BIO_push(b64, mem);
BIO_write(b64, hash, (int)hashLen);
BIO_flush(b64);
char *encodedData = nullptr;
long encodedLen = BIO_get_mem_data(mem, &encodedData);
std::string result(encodedData, encodedLen);
BIO_free_all(b64);
return result;
}
// Fetches the signer's public key: TXT record at
// <selector>._domainkey.<domain> (RFC 6376 3.6.1/3.6.2.2), tag
// format "v=DKIM1; k=rsa; p=<base64 SubjectPublicKeyInfo>" - v=
// and k= are optional (default to exactly this), only p= is
// actually required. An empty p= is a published, deliberate key
// revocation (6376 3.6.1) and correctly fails to load as a key.
EVP_PKEY *fetchPublicKey(const std::string &selector, const std::string &domain) {
std::string queryName = selector + "._domainkey." + domain;
unsigned char answer[4096];
int len = res_query(queryName.c_str(), C_IN, T_TXT, answer, sizeof(answer));
if (len <= 0)
return nullptr;
ns_msg handle;
if (ns_initparse(answer, len, &handle) != 0)
return nullptr;
std::string record;
int count = ns_msg_count(handle, ns_s_an);
for (int i = 0; i < count && record.empty(); ++i) {
ns_rr rr;
if (ns_parserr(&handle, ns_s_an, i, &rr) != 0 || ns_rr_type(rr) != ns_t_txt)
continue;
const unsigned char *rdata = ns_rr_rdata(rr);
int rdlen = ns_rr_rdlen(rr);
std::string full;
int pos = 0;
while (pos < rdlen) {
int segLen = rdata[pos];
++pos;
if (pos + segLen > rdlen)
break;
full.append((const char *)(rdata + pos), segLen);
pos += segLen;
}
if (full.find("p=") != std::string::npos)
record = full;
}
if (record.empty())
return nullptr;
std::string p;
std::istringstream stream(record);
std::string tag;
while (std::getline(stream, tag, ';')) {
size_t start = tag.find_first_not_of(" \t");
if (start == std::string::npos)
continue;
tag = tag.substr(start);
if (tag.rfind("p=", 0) == 0) {
p = tag.substr(2);
// Public key TXT records are often split across several
// quoted strings by the zone editor for readability -
// res_query()/ns_parserr() already concatenated the RR's
// own multiple character-strings, but stray whitespace
// between them (or trailing junk after the base64) still
// needs stripping before base64 decoding.
std::string cleaned;
for (char c : p)
if (!std::isspace((unsigned char)c))
cleaned += c;
p = cleaned;
}
}
if (p.empty())
return nullptr; // revoked key (6376 3.6.1) or malformed record
std::string der = base64Decode(p);
if (der.empty())
return nullptr;
const unsigned char *derPtr = (const unsigned char *)der.data();
return d2i_PUBKEY(nullptr, &derPtr, (long)der.size());
}
}
std::vector<DKIMVerifier::SignatureResult> DKIMVerifier::verify(const std::string &message) {
std::vector<SignatureResult> results;
std::vector<RawHeader> headers = parseRawHeaders(message);
std::string body = bodyOf(message);
for (size_t sigIndex = 0; sigIndex < headers.size(); ++sigIndex) {
if (headers[sigIndex].name.size() != 14) continue;
bool isDkimSig = true;
{
static const char *want = "DKIM-Signature";
for (size_t i = 0; i < 14 && isDkimSig; ++i)
isDkimSig = std::tolower((unsigned char)headers[sigIndex].name[i]) == std::tolower((unsigned char)want[i]);
}
if (!isDkimSig)
continue;
SignatureResult result;
// Parse this DKIM-Signature's own tags.
std::string tagStr = headers[sigIndex].unfoldedValue;
std::string a = "rsa-sha256", c = "simple/simple", d, s, h, bh, b;
std::istringstream stream(tagStr);
std::string part;
while (std::getline(stream, part, ';')) {
size_t start = part.find_first_not_of(" \t");
if (start == std::string::npos)
continue;
part = part.substr(start);
size_t eq = part.find('=');
if (eq == std::string::npos)
continue;
std::string key = part.substr(0, eq);
std::string value = part.substr(eq + 1);
while (!value.empty() && std::isspace((unsigned char)value.back()))
value.pop_back();
if (key == "a") a = value;
else if (key == "c") c = value;
else if (key == "d") d = value;
else if (key == "s") s = value;
else if (key == "h") h = value;
else if (key == "bh") bh = value;
else if (key == "b") { std::string cleaned; for (char ch : value) if (!std::isspace((unsigned char)ch)) cleaned += ch; b = cleaned; }
}
result.domain = d;
result.selector = s;
if (d.empty() || s.empty() || bh.empty() || b.empty()) {
results.push_back(result); // valid=false: malformed signature
continue;
}
bool simpleHeader = (c.rfind("simple", 0) == 0);
bool simpleBody = (c.find('/') != std::string::npos) ? (c.substr(c.find('/') + 1).rfind("simple", 0) == 0)
: simpleHeader; // "c=simple" alone means simple/simple
bool useSha1 = (a == "rsa-sha1");
std::string computedBH = sha1or256Base64(useSha1, canonicalizeBody(simpleBody, body));
if (computedBH != bh) {
results.push_back(result); // body was altered in transit - valid=false
continue;
}
// Build the signing input the same way DKIMSigner does: every
// header named in h=, in that order, canonicalized per c='s
// header algorithm, followed by this DKIM-Signature field
// itself (with b= emptied) - also canonicalized, and, for
// simple canonicalization specifically, using the ORIGINAL
// header exactly as received up through "b=" (nothing after
// it), not a re-synthesized one, since simple canonicalization
// forbids changing header text at all.
std::string signingInput;
std::istringstream hstream(h);
std::string wanted;
while (std::getline(hstream, wanted, ':')) {
for (auto &hdr : headers) {
if (hdr.name.size() == wanted.size()) {
bool match = true;
for (size_t i = 0; i < wanted.size() && match; ++i)
match = std::tolower((unsigned char)hdr.name[i]) == std::tolower((unsigned char)wanted[i]);
if (match) {
signingInput += canonicalizeHeaderField(simpleHeader, hdr);
break;
}
}
}
}
size_t bPos = tagStr.rfind("b=");
std::string tagsUpToB = tagStr.substr(0, bPos + 2);
std::string dkimFieldCanon;
if (simpleHeader) {
// rawText already reads "DKIM-Signature: v=1; ...; b=..." in
// full (name, colon, and value all together, folding
// preserved) - simple canonicalization requires using that
// exact original text unmodified, just truncated right after
// the literal "b=" so the actual signature value isn't
// included (RFC 6376 3.7).
size_t rawBPos = headers[sigIndex].rawText.find("b=");
dkimFieldCanon = (rawBPos == std::string::npos)
? canonicalizeSyntheticHeader(true, headers[sigIndex].name, tagsUpToB)
: headers[sigIndex].rawText.substr(0, rawBPos + 2);
} else {
dkimFieldCanon = canonicalizeSyntheticHeader(false, headers[sigIndex].name, tagsUpToB);
}
// The CRLF that would terminate this header field MUST NOT be
// included (RFC 6376 3.7) - it doesn't exist yet from the
// signer's perspective. canonicalizeSyntheticHeader's relaxed
// path always appends one; the simple path (a rawText prefix
// ending right at "b=") never had one to begin with.
if (dkimFieldCanon.size() >= 2 && dkimFieldCanon.substr(dkimFieldCanon.size() - 2) == "\r\n")
dkimFieldCanon.erase(dkimFieldCanon.size() - 2);
signingInput += dkimFieldCanon;
EVP_PKEY *pubkey = fetchPublicKey(s, d);
if (!pubkey) {
results.push_back(result); // no key published, or revoked - valid=false
continue;
}
std::string sigBytes = base64Decode(b);
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
int rc = 0;
if (EVP_DigestVerifyInit(mdctx, nullptr, useSha1 ? EVP_sha1() : EVP_sha256(), nullptr, pubkey) == 1)
rc = EVP_DigestVerify(mdctx, (const unsigned char *)sigBytes.data(), sigBytes.size(),
(const unsigned char *)signingInput.data(), signingInput.size());
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(pubkey);
result.valid = (rc == 1);
results.push_back(result);
}
return results;
}
}
+50
View File
@@ -0,0 +1,50 @@
#ifndef __DKIMVerifier_h__
#define __DKIMVerifier_h__
#include <string>
#include <vector>
namespace mail {
///
/// DKIMVerifier checks the DKIM-Signature header(s) (RFC 6376) on an
/// already-fully-received message: for each one present, fetches the
/// claimed signer's public key from DNS and verifies the signature
/// against it. Exists specifically to support DMARC's DKIM-alignment
/// check (DMARCChecker) - a signature that verifies but whose d=
/// domain does not align with the message's From: header doesn't
/// make the message DMARC-authenticated via DKIM, so both the
/// cryptographic result AND the signing domain are reported back for
/// DMARC to judge alignment itself.
///
/// A message may carry more than one DKIM-Signature (e.g. one from
/// the original sender, one added by a mailing list) - every one
/// present is verified independently.
///
/// Unlike DKIMSigner (which only ever produces relaxed/relaxed,
/// rsa-sha256 signatures of its own), this has to interoperate with
/// whatever a real remote sender actually used, so it supports all
/// four canonicalization combinations (simple/relaxed for header/body)
/// a real DKIM-Signature's c= tag can specify. Does not implement
/// signature expiry (x=) checking or Ed25519 (RFC 8463) - both real
/// gaps, but the overwhelming majority of real-world DKIM signatures
/// today are plain rsa-sha256 with no expiry set.
///
class DKIMVerifier {
public:
struct SignatureResult {
bool valid = false;
std::string domain; // d= tag - the identity DMARC needs to align against
std::string selector; // s= tag
};
static std::vector<SignatureResult> verify(const std::string &message);
};
}
#endif
+171
View File
@@ -0,0 +1,171 @@
#include "DMARCChecker.h"
#include <arpa/nameser.h>
#include <cctype>
#include <resolv.h>
#include <sstream>
namespace mail {
namespace {
std::vector<std::string> lookupDMARCRecords(const std::string &domain) {
std::vector<std::string> records;
std::string queryName = "_dmarc." + domain;
unsigned char answer[4096];
int len = res_query(queryName.c_str(), C_IN, T_TXT, answer, sizeof(answer));
if (len <= 0)
return records;
ns_msg handle;
if (ns_initparse(answer, len, &handle) != 0)
return records;
int count = ns_msg_count(handle, ns_s_an);
for (int i = 0; i < count; ++i) {
ns_rr rr;
if (ns_parserr(&handle, ns_s_an, i, &rr) != 0 || ns_rr_type(rr) != ns_t_txt)
continue;
const unsigned char *rdata = ns_rr_rdata(rr);
int rdlen = ns_rr_rdlen(rr);
std::string full;
int pos = 0;
while (pos < rdlen) {
int segLen = rdata[pos];
++pos;
if (pos + segLen > rdlen)
break;
full.append((const char *)(rdata + pos), segLen);
pos += segLen;
}
std::string lower = full;
for (char &c : lower)
c = std::tolower((unsigned char)c);
if (lower.rfind("v=dmarc1", 0) == 0)
records.push_back(full);
}
return records;
}
void parseTags(const std::string &record, DMARCChecker::Policy &p) {
std::istringstream stream(record);
std::string part;
while (std::getline(stream, part, ';')) {
size_t start = part.find_first_not_of(" \t");
if (start == std::string::npos)
continue;
part = part.substr(start);
size_t eq = part.find('=');
if (eq == std::string::npos)
continue;
std::string key = part.substr(0, eq);
std::string value = part.substr(eq + 1);
while (!value.empty() && std::isspace((unsigned char)value.back()))
value.pop_back();
for (char &c : key)
c = std::tolower((unsigned char)c);
if (key == "p")
p.policy = value;
else if (key == "sp")
p.subdomainPolicy = value;
else if (key == "adkim")
p.strictDKIM = (value == "s");
else if (key == "aspf")
p.strictSPF = (value == "s");
else if (key == "pct")
p.percent = atoi(value.c_str());
}
if (p.subdomainPolicy.empty())
p.subdomainPolicy = p.policy;
}
}
std::string DMARCChecker::organizationalDomain(const std::string &domain) {
std::vector<std::string> labels;
std::istringstream stream(domain);
std::string label;
while (std::getline(stream, label, '.'))
if (!label.empty())
labels.push_back(label);
if (labels.size() <= 2)
return domain;
return labels[labels.size() - 2] + "." + labels[labels.size() - 1];
}
bool DMARCChecker::aligned(const std::string &identityDomain, const std::string &fromDomain, bool strict) {
if (identityDomain.empty() || fromDomain.empty())
return false;
auto lower = [](std::string s) { for (char &c : s) c = std::tolower((unsigned char)c); return s; };
if (strict)
return lower(identityDomain) == lower(fromDomain);
return lower(organizationalDomain(identityDomain)) == lower(organizationalDomain(fromDomain));
}
bool DMARCChecker::lookupPolicy(const std::string &domain, Policy &policyOut) {
std::vector<std::string> records = lookupDMARCRecords(domain);
if (records.size() == 1) {
parseTags(records[0], policyOut);
policyOut.found = true;
return true;
}
if (records.size() > 1)
return false; // RFC 7489 6.6.3: more than one record is treated as none published
// RFC 7489 6.6.3: no record at the exact domain falls back to the
// organizational domain's own record (using ITS sp=, or p= if sp=
// is absent, as the effective policy for this subdomain) - but
// only when domain actually IS a subdomain of something else, or
// this would just re-query the same name again.
std::string org = organizationalDomain(domain);
if (org == domain)
return false;
std::vector<std::string> orgRecords = lookupDMARCRecords(org);
if (orgRecords.size() != 1)
return false;
Policy orgPolicy;
parseTags(orgRecords[0], orgPolicy);
policyOut = orgPolicy;
policyOut.policy = orgPolicy.subdomainPolicy; // sp= (or p=, via parseTags' own fallback) governs here
policyOut.found = true;
return true;
}
DMARCChecker::Result DMARCChecker::evaluate(const std::string &fromDomain, const std::string &spfDomain,
SPFChecker::Result spfResult,
const std::vector<DKIMVerifier::SignatureResult> &dkimResults,
Policy &appliedPolicy) {
if (fromDomain.empty() || !lookupPolicy(fromDomain, appliedPolicy))
return Result::None;
bool spfAligned = (spfResult == SPFChecker::Result::Pass) && aligned(spfDomain, fromDomain, appliedPolicy.strictSPF);
bool dkimAligned = false;
for (auto &d : dkimResults)
if (d.valid && aligned(d.domain, fromDomain, appliedPolicy.strictDKIM)) {
dkimAligned = true;
break;
}
return (spfAligned || dkimAligned) ? Result::Pass : Result::Fail;
}
std::string DMARCChecker::resultName(Result result) {
switch (result) {
case Result::Pass: return "pass";
case Result::Fail: return "fail";
case Result::None: return "none";
}
return "none";
}
std::string DMARCChecker::authenticationResultsHeader(SPFChecker::Result spfResult, const std::string &spfDomain,
const std::vector<DKIMVerifier::SignatureResult> &dkimResults,
Result dmarcResult, const std::string &fromDomain,
const std::string &receivingHost) {
std::string h = "Authentication-Results: " + receivingHost + ";\r\n";
h += "\tspf=" + SPFChecker::resultName(spfResult) + " smtp.mailfrom=" + spfDomain + ";\r\n";
for (auto &d : dkimResults)
h += "\tdkim=" + std::string(d.valid ? "pass" : "fail") + " header.d=" + d.domain + " header.s=" + d.selector + ";\r\n";
h += "\tdmarc=" + resultName(dmarcResult) + " header.from=" + fromDomain + ";\r\n";
return h;
}
}
+93
View File
@@ -0,0 +1,93 @@
#ifndef __DMARCChecker_h__
#define __DMARCChecker_h__
#include "DKIMVerifier.h"
#include "SPFChecker.h"
#include <string>
#include <vector>
namespace mail {
///
/// DMARCChecker implements RFC 7489: a message is DMARC-authenticated
/// if EITHER SPF or DKIM passed AND is "aligned" with the RFC 5322
/// From: header's domain (the identity a human actually sees) - SPF
/// alone checks the envelope sender, DKIM alone checks whatever
/// domain signed it, and neither by itself stops a message whose
/// From: header claims a domain that had nothing to do with either
/// check. Requires both SPFChecker's result and DKIMVerifier's
/// results as input, since DMARC is defined entirely in terms of
/// those two other mechanisms' outcomes plus the alignment check.
///
/// Not implemented: the RFC 7489 Public Suffix List-based
/// organizational-domain derivation for relaxed alignment - the
/// PSL is a several-thousand-entry external data file this codebase
/// has no copy of and no fetch mechanism for. Approximated instead
/// by comparing the last two dot-separated labels (e.g. treating
/// "mail.example.com" and "example.com" as aligned via
/// "example.com"), which is correct for ordinary domains but wrong
/// for registrations directly under a multi-label public suffix
/// (e.g. "example.co.uk" and "other.co.uk" would incorrectly be
/// treated as aligned via "co.uk"). Exact (strict) alignment,
/// requested via adkim=s/aspf=s, is unaffected by this and is exact
/// either way.
///
class DMARCChecker {
public:
enum class Result { Pass, Fail, None };
struct Policy {
bool found = false;
std::string policy = "none"; // p=
std::string subdomainPolicy; // sp= (defaults to policy if absent)
bool strictDKIM = false; // adkim=s (default r, relaxed)
bool strictSPF = false; // aspf=s (default r, relaxed)
int percent = 100; // pct=
};
///
/// fromDomain: the RFC 5322 From: header's domain (what a human
/// reading the message actually sees as the sender).
/// spfDomain/spfResult: the identity SPF was actually evaluated
/// against (RFC 7208 2.4 - the MAIL FROM domain, or HELO's for a
/// null reverse-path) and its result.
/// dkimResults: every DKIM-Signature DKIMVerifier found on this
/// message, verified or not.
/// appliedPolicy is filled in with the policy that was actually
/// looked up (found==false if none was published at all - the
/// domain isn't participating in DMARC, so this always resolves
/// to Result::None regardless of alignment).
///
static Result evaluate(const std::string &fromDomain, const std::string &spfDomain, SPFChecker::Result spfResult,
const std::vector<DKIMVerifier::SignatureResult> &dkimResults, Policy &appliedPolicy);
static std::string resultName(Result result);
///
/// A DMARC-aware Authentication-Results header (RFC 8601) -
/// summarizes spf=/dkim=/dmarc= together, the modern convention
/// downstream filtering actually looks for (Received-SPF alone,
/// which __SMTP_MAIL.cpp already adds, predates DMARC and doesn't
/// record dkim/dmarc results at all).
///
static std::string authenticationResultsHeader(SPFChecker::Result spfResult, const std::string &spfDomain,
const std::vector<DKIMVerifier::SignatureResult> &dkimResults,
Result dmarcResult, const std::string &fromDomain,
const std::string &receivingHost);
private:
static bool lookupPolicy(const std::string &domain, Policy &policyOut);
static std::string organizationalDomain(const std::string &domain);
static bool aligned(const std::string &identityDomain, const std::string &fromDomain, bool strict);
};
}
#endif
+154
View File
@@ -0,0 +1,154 @@
#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;
}
}
}
+125
View File
@@ -0,0 +1,125 @@
#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
+33 -2
View File
@@ -6,8 +6,39 @@
namespace mail {
int IMAPCommand::processCommand(coreutils::ZString &request, core::TCPSession &session) {
return processCommand(request, (IMAPSession &)session, (IMAPServer &)session.server);
// session.server is a TCPServer* (pointer) - C-style-casting the
// POINTER VALUE directly to a reference type does not dereference
// it. Per C++'s reference-cast rules this falls back to
// reinterpret_cast<T&>, which for an expression that is not
// itself an object is *reinterpret_cast<T*>(&expr) - i.e. it took
// the address of the `session.server` POINTER FIELD ITSELF (8
// bytes, wherever that field happens to live inside the
// TCPSession/IMAPSession object) and read an entire IMAPServer's
// worth of memory starting there, not the real IMAPServer object
// the pointer actually points to. Confirmed live via gdb: fields
// read off the "IMAPServer" this produced were complete garbage
// (a null EPoll reference, a socket descriptor of -136794095).
// The compiler's own "does not dereference pointer" warning on
// this exact line, present on every build all session, was
// reporting precisely this. Dereferencing the pointer first
// (*session.server, a real TCPServer&) makes the cast to
// IMAPServer& a normal, correct base-to-derived downcast.
// CommandList::processRequest() (ServerCore) calls request.reset()
// right before dispatch - the cursor lands at the true start of
// the whole command line, which STILL includes the command word
// itself ("LOGIN", "FETCH", ...), not just past it. SMTP handlers
// never noticed because they read arguments via request[N] (the
// pre-split list, built independently of cursor position) rather
// than the cursor - every IMAP handler here parses off the raw
// cursor instead (see IMAPArgs.h's own doc comment for why), so
// without this, every handler's first nextArgument() call was
// reading the command word itself as if it were the first real
// argument (confirmed live: LOGIN "user" "pass" produced
// userArg=="LOGIN", passArg=="user", with "pass" never read at
// all). Discarding it here, once, centrally, is simpler and less
// error-prone than every individual handler remembering to do it.
request.getTokenExclude(" ");
return processCommand(request, (IMAPSession &)session, (IMAPServer &)(*session.server));
}
}
+34 -4
View File
@@ -2,10 +2,13 @@
# define __IMAPServer_h__
# include "TCPServer.h"
# include "TLSInfo.h"
# include "Exception.h"
# include "IMAPSession.h"
# include "MailFileSystem.h"
# include "__IMAP_APPEND.h"
# include "__IMAP_AUTHENTICATE.h"
# include "__IMAP_CAPABILITY.h"
# include "__IMAP_CHECK.h"
# include "__IMAP_CLOSE.h"
# include "__IMAP_COPY.h"
@@ -16,7 +19,9 @@
# include "__IMAP_FETCH.h"
# include "__IMAP_LIST.h"
# include "__IMAP_LOGIN.h"
# include "__IMAP_LOGOUT.h"
# include "__IMAP_LSUB.h"
# include "__IMAP_NOOP.h"
# include "__IMAP_RENAME.h"
# include "__IMAP_SEARCH.h"
# include "__IMAP_SELECT.h"
@@ -32,21 +37,31 @@ namespace mail {
class IMAPServer : public core::TCPServer {
public:
IMAPServer(core::EPoll &epoll, std::string hostName, MailFileSystem &mailFileSystem, core::IPAddress ipAddress)
: TCPServer(epoll, ipAddress, " ", 10, "IMAP Server"), mailFileSystem(mailFileSystem), hostName(hostName) {
IMAPServer(core::EPoll &epoll, std::string hostName, MailFileSystem &mailFileSystem,
core::IPAddress ipAddress, core::TLSInfo *tlsInfo)
: TCPServer(epoll, ipAddress, tlsInfo, " ", 10, "IMAP Server"), mailFileSystem(mailFileSystem), hostName(hostName) {
commands.add(_imap_append, "APPEND");
commands.add(_imap_authenticate, "AUTHENTCATE");
// Was registered as "AUTHENTCATE" (missing the I) - no
// correctly-spelled client request could ever match it.
commands.add(_imap_authenticate, "AUTHENTICATE");
// RFC 3501 6.1.1/6.1.2/6.1.3: CAPABILITY, NOOP, and LOGOUT MUST
// be supported in every connection state - none of the three
// were registered at all before.
commands.add(_imap_capability, "CAPABILITY");
commands.add(_imap_check, "CHECK");
commands.add(_imap_close, "CLOSE");
commands.add(_imap_copy, "COPY");
commands.add(_imap_create, "CREATE");
commands.add(_imap_delete, "DELETE");
commands.add(_imap_examine, "EXAAMINE");
// Was registered as "EXAAMINE" (double A) - same problem.
commands.add(_imap_examine, "EXAMINE");
commands.add(_imap_expunge, "EXPUNGE");
commands.add(_imap_fetch, "FETCH");
commands.add(_imap_list, "LIST");
commands.add(_imap_login, "LOGIN");
commands.add(_imap_logout, "LOGOUT");
commands.add(_imap_lsub, "LSUB");
commands.add(_imap_noop, "NOOP");
commands.add(_imap_rename, "RENAME");
commands.add(_imap_search, "SEARCH");
commands.add(_imap_select, "SELECT");
@@ -61,9 +76,22 @@ namespace mail {
MailFileSystem &mailFileSystem;
std::string hostName;
// Without this override, TCPServer's default getSocketAccept()
// returns a plain TCPSession for every accepted connection, not
// an IMAPSession - IMAPSession::onConnected() (the greeting
// banner) then never fires, and IMAPCommand's session casts
// ((IMAPSession &)session) reinterpret a TCPSession's memory as
// an IMAPSession, undefined behavior the moment any handler
// touches a member IMAPSession actually has and TCPSession
// doesn't.
IMAPSession *getSocketAccept(core::EPoll &ePoll) override {
return new IMAPSession(ePoll, *this);
}
private:
__IMAP_APPEND _imap_append;
__IMAP_AUTHENTICATE _imap_authenticate;
__IMAP_CAPABILITY _imap_capability;
__IMAP_CHECK _imap_check;
__IMAP_CLOSE _imap_close;
__IMAP_COPY _imap_copy;
@@ -74,7 +102,9 @@ namespace mail {
__IMAP_FETCH _imap_fetch;
__IMAP_LIST _imap_list;
__IMAP_LOGIN _imap_login;
__IMAP_LOGOUT _imap_logout;
__IMAP_LSUB _imap_lsub;
__IMAP_NOOP _imap_noop;
__IMAP_RENAME _imap_rename;
__IMAP_SEARCH _imap_search;
__IMAP_SELECT _imap_select;
+64 -2
View File
@@ -1,6 +1,9 @@
#ifndef __IMAPSession_h__
# define __IMAPSession_h__
# include "MailFolder.h"
# include <functional>
# define CRLF "\r\n"
namespace mail {
@@ -9,17 +12,76 @@ namespace mail {
public:
IMAPSession(core::EPoll &ePoll, core::TCPServer &server)
: TCPSession(ePoll, server, "IMAP Client Session") {}
: TCPSession(ePoll, &server, "IMAP Client Session") {}
enum State { NOT_AUTHENTICATED, AUTHENTICATED, SELECTED };
std::string clientDomainName;
std::string userName;
std::string password;
std::string tag;
State state = NOT_AUTHENTICATED;
// Valid only while state == SELECTED - the folder SELECT/EXAMINE
// last loaded. readOnly is true for EXAMINE (STORE/EXPUNGE must
// be refused), false for SELECT.
MailFolder mailbox;
std::string selectedFolderName;
bool readOnly = false;
// APPEND's message-body literal is the one place raw bytes (not
// command lines) arrive - see TCPSocket::setBlockSize()/
// onBlockReceived(), unused anywhere else in this codebase before
// now. __IMAP_APPEND sets this to a closure that does the actual
// mailbox write (kept out of IMAPSession itself so this class
// doesn't need to know about MailFileSystem/MailFolder specifics -
// it only knows "a block finished, hand it off"), then calls
// setBlockSize(n) and sends the "+ OK" continuation. Cleared
// before invoking, so a handler that itself needs another literal
// (not currently used by anything) can safely set a new one.
std::function<void(coreutils::ZString &)> pendingBlockHandler;
void onBlockReceived(coreutils::ZString &block) override {
if (pendingBlockHandler) {
auto handler = pendingBlockHandler;
pendingBlockHandler = nullptr;
handler(block);
}
}
// Used to falsely claim to be Dovecot and advertise a capability
// list (LITERAL+/SASL-IR/LOGIN-REFERRALS/ID/ENABLE/IDLE/STARTTLS/
// AUTH=PLAIN/AUTH=LOGIN) none of which had a real handler behind
// it - see __IMAP_CAPABILITY.cpp's own doc comment for the same
// reasoning applied to the CAPABILITY command's response.
void onConnected() override {
out << "* OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE STARTTLS AUTH=PLAIN AUTH=LOGIN] Dovecot (Ubuntu) ready." << CRLF;
out << "* OK [CAPABILITY IMAP4rev1] BARANTMail ready." << CRLF;
}
protected:
// Every IMAP command line is client-tag-prefixed ("a001 LOGIN ...",
// RFC 3501 2.2.1) - the tag is an arbitrary string the client
// chooses, not a fixed command name, so it can never appear in the
// command table CommandList::processRequest() dispatches on
// (ServerCore/CommandList.cpp looks up request[0], and for every
// real client request[0] is this tag, not the command word).
// Confirmed: with no tag-stripping anywhere, request[0] never
// matched any registered command, the lookup threw, and the
// catch-all in processRequest() silently swallowed it - no IMAP
// command from any spec-compliant client ever reached a handler.
// Extracting the tag here, before the generic space-delimited
// dispatch runs, leaves the command word where the base
// TCPSession::protocol()/CommandList expects to find it.
void protocol(coreutils::ZString &data) override {
if (data.getLength() == 0) {
TCPSession::protocol(data);
return;
}
tag = data.getTokenExclude(" ").str();
data.skipWhitespace();
coreutils::ZString command = data.unparsed();
TCPSession::protocol(command);
}
};
+68
View File
@@ -2,11 +2,19 @@
#include "MailFileSystem.h"
# include "INotify.h"
# include "Log.h"
# include "File.h"
# include <filesystem>
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) {
@@ -27,6 +35,16 @@ namespace mail {
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;
@@ -45,4 +63,54 @@ namespace mail {
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<coreutils::MString> MailFileSystem::listFolders(coreutils::ZString &mailbox) {
std::vector<coreutils::MString> 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;
}
}
+22
View File
@@ -5,6 +5,7 @@
# include <sys/types.h>
# include <sys/stat.h>
# include <unistd.h>
# include <vector>
namespace mail {
@@ -18,10 +19,31 @@ namespace mail {
std::vector<coreutils::MString> getAliasMailboxes(coreutils::ZString &alias);
coreutils::ZString& getMailPath();
coreutils::ZString& getQueuePath();
coreutils::ZString& getSendLogPath();
// Shared by every protocol's login (SMTP AUTH LOGIN, IMAP LOGIN,
// POP3 USER/PASS) so there is exactly one place that reads
// <mailbox>/.password and compares it - previously
// __SMTP_AUTH::authLogin() carried this logic alone, and IMAP/POP3
// login would otherwise have needed their own copies. Still a
// plaintext file comparison (a separately tracked, pre-existing
// gap) - not something this change fixes, just no longer
// triplicated.
bool checkPassword(coreutils::ZString &userName, coreutils::ZString &password);
// Folder names (Inbox/Spam/Trash/anything CREATEd) as immediate
// subdirectories of a mailbox's own directory - used by IMAP
// LIST/LSUB/SELECT/CREATE/DELETE/RENAME to enumerate and validate
// folders without each command re-implementing the same
// directory scan and dotfile-skipping rule.
std::vector<coreutils::MString> listFolders(coreutils::ZString &mailbox);
coreutils::MString getFolderPath(coreutils::ZString &mailbox, std::string folder);
bool ifFolderExists(coreutils::ZString &mailbox, std::string folder);
private:
coreutils::ZString &mailPath;
coreutils::MString queuePath;
coreutils::MString sendLogPath;
struct stat statbuf;
};
+299
View File
@@ -0,0 +1,299 @@
#include "MailFolder.h"
#include <algorithm>
#include <ctime>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <sys/stat.h>
#include <unistd.h>
#include <filesystem>
namespace mail {
namespace {
std::string joinFlags(const std::vector<std::string> &flags) {
std::string out;
for (size_t i = 0; i < flags.size(); ++i) {
if (i > 0)
out += ",";
out += flags[i];
}
return out;
}
std::vector<std::string> splitFlags(const std::string &blob) {
std::vector<std::string> flags;
std::istringstream stream(blob);
std::string part;
while (std::getline(stream, part, ','))
if (!part.empty())
flags.push_back(part);
return flags;
}
bool hasFlag(const std::vector<std::string> &flags, const std::string &flag) {
return std::find(flags.begin(), flags.end(), flag) != flags.end();
}
uint32_t readCounterFile(const std::string &path, uint32_t defaultValue) {
std::ifstream in(path);
uint32_t value = 0;
if (in >> value)
return value;
return defaultValue;
}
void writeCounterFile(const std::string &path, uint32_t value) {
std::ofstream out(path, std::ios::trunc);
out << value;
}
}
bool MailFolder::load(std::string folderPath) {
path = folderPath;
struct stat folderStat;
if (stat(path.c_str(), &folderStat) != 0 || !S_ISDIR(folderStat.st_mode))
return false;
// UIDVALIDITY is generated once and never changes for the life of
// this folder directory - reusing the folder's own creation time
// is a fine source of a value that's stable, unique enough in
// practice, and never zero.
mUidValidity = readCounterFile(path + "/.uidvalidity", 0);
if (mUidValidity == 0) {
mUidValidity = (uint32_t)time(nullptr);
writeCounterFile(path + "/.uidvalidity", mUidValidity);
}
mUidNext = readCounterFile(path + "/.uidnext", 1);
if (mUidNext < 1)
mUidNext = 1;
mMessages.clear();
loadIndexFile();
reconcileWithDisk();
save();
return true;
}
void MailFolder::loadIndexFile() {
std::ifstream in(path + "/.imapindex");
if (!in)
return;
std::string line;
while (std::getline(in, line)) {
if (line.empty())
continue;
std::istringstream stream(line);
Message message;
std::string uidStr, dateStr, flagsBlob;
stream >> message.fileName >> uidStr >> dateStr;
std::getline(stream, flagsBlob);
// The flags blob (if any) is whatever's left after the third
// field, minus the single separating space istream>> already
// consumed up to.
if (!flagsBlob.empty() && flagsBlob[0] == ' ')
flagsBlob.erase(0, 1);
message.uid = (uint32_t)std::stoul(uidStr);
message.internalDate = (time_t)std::stoll(dateStr);
message.flags = splitFlags(flagsBlob);
mMessages.push_back(message);
}
}
void MailFolder::reconcileWithDisk() {
std::vector<std::string> onDisk;
if (std::filesystem::is_directory(path)) {
for (const auto &entry : std::filesystem::directory_iterator(path)) {
if (!entry.is_regular_file())
continue;
std::string name = entry.path().filename().string();
if (!name.empty() && name[0] != '.')
onDisk.push_back(name);
}
}
// Drop indexed messages no longer present on disk (expunged some
// other way than MailFolder::expunge(), or the file simply never
// made it - either way, nothing to show for it).
mMessages.erase(std::remove_if(mMessages.begin(), mMessages.end(),
[&](const Message &m) {
return std::find(onDisk.begin(), onDisk.end(), m.fileName) == onDisk.end();
}), mMessages.end());
// Anything on disk not yet indexed is newly delivered mail (or a
// folder being indexed for the very first time) - assign it a UID
// now and mark it \Recent for this one load() only.
for (const std::string &name : onDisk) {
bool indexed = std::any_of(mMessages.begin(), mMessages.end(),
[&](const Message &m) { return m.fileName == name; });
if (indexed)
continue;
Message message;
message.fileName = name;
message.uid = mUidNext++;
message.recent = true;
struct stat fileStat;
if (stat((path + "/" + name).c_str(), &fileStat) == 0) {
message.size = (size_t)fileStat.st_size;
message.internalDate = fileStat.st_mtime;
} else {
message.internalDate = time(nullptr);
}
mMessages.push_back(message);
}
// Fill in size for every message (including ones that were
// already indexed - size isn't persisted, it's cheap to stat).
for (Message &message : mMessages) {
struct stat fileStat;
if (stat((path + "/" + message.fileName).c_str(), &fileStat) == 0)
message.size = (size_t)fileStat.st_size;
}
std::sort(mMessages.begin(), mMessages.end(),
[](const Message &a, const Message &b) { return a.uid < b.uid; });
}
void MailFolder::save() {
writeCounterFile(path + "/.uidvalidity", mUidValidity);
writeCounterFile(path + "/.uidnext", mUidNext);
std::ofstream out(path + "/.imapindex", std::ios::trunc);
for (const Message &message : mMessages)
out << message.fileName << " " << message.uid << " " << (long long)message.internalDate
<< " " << joinFlags(message.flags) << "\n";
}
uint32_t MailFolder::seq(uint32_t uid) const {
for (size_t i = 0; i < mMessages.size(); ++i)
if (mMessages[i].uid == uid)
return (uint32_t)(i + 1);
return 0;
}
uint32_t MailFolder::uidForSeq(uint32_t seqNum) const {
if (seqNum < 1 || seqNum > mMessages.size())
return 0;
return mMessages[seqNum - 1].uid;
}
MailFolder::Message *MailFolder::find(uint32_t uid) {
for (Message &message : mMessages)
if (message.uid == uid)
return &message;
return nullptr;
}
size_t MailFolder::recentCount() const {
return (size_t)std::count_if(mMessages.begin(), mMessages.end(),
[](const Message &m) { return m.recent; });
}
size_t MailFolder::unseenCount() const {
return (size_t)std::count_if(mMessages.begin(), mMessages.end(),
[](const Message &m) { return !hasFlag(m.flags, "\\Seen"); });
}
uint32_t MailFolder::firstUnseenSeq() const {
for (size_t i = 0; i < mMessages.size(); ++i)
if (!hasFlag(mMessages[i].flags, "\\Seen"))
return (uint32_t)(i + 1);
return 0;
}
std::string MailFolder::readMessage(uint32_t uid) {
Message *message = find(uid);
if (!message)
return "";
std::ifstream in(path + "/" + message->fileName, std::ios::binary);
std::ostringstream out;
out << in.rdbuf();
return out.str();
}
void MailFolder::setFlags(uint32_t uid, const std::vector<std::string> &flags) {
Message *message = find(uid);
if (!message)
return;
message->flags = flags;
save();
}
void MailFolder::addFlags(uint32_t uid, const std::vector<std::string> &flags) {
Message *message = find(uid);
if (!message)
return;
for (const std::string &flag : flags)
if (!hasFlag(message->flags, flag))
message->flags.push_back(flag);
save();
}
void MailFolder::removeFlags(uint32_t uid, const std::vector<std::string> &flags) {
Message *message = find(uid);
if (!message)
return;
message->flags.erase(std::remove_if(message->flags.begin(), message->flags.end(),
[&](const std::string &f) { return hasFlag(flags, f); }), message->flags.end());
save();
}
void MailFolder::deleteMessage(uint32_t uid) {
Message *message = find(uid);
if (!message)
return;
unlink((path + "/" + message->fileName).c_str());
mMessages.erase(std::remove_if(mMessages.begin(), mMessages.end(),
[&](const Message &m) { return m.uid == uid; }), mMessages.end());
save();
}
std::string MailFolder::generateFileName() {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
std::stringstream ss;
ss << ts.tv_sec << "." << std::setfill('0') << std::setw(9) << ts.tv_nsec;
return ss.str();
}
uint32_t MailFolder::append(const std::string &data, const std::vector<std::string> &flags, time_t internalDate) {
std::string fileName = generateFileName();
std::ofstream out(path + "/" + fileName, std::ios::binary | std::ios::trunc);
out << data;
out.close();
Message message;
message.fileName = fileName;
message.uid = mUidNext++;
message.flags = flags;
message.size = data.size();
message.internalDate = internalDate != 0 ? internalDate : time(nullptr);
mMessages.push_back(message);
std::sort(mMessages.begin(), mMessages.end(),
[](const Message &a, const Message &b) { return a.uid < b.uid; });
save();
return message.uid;
}
std::vector<uint32_t> MailFolder::expunge() {
std::vector<Message> toDelete;
for (const Message &message : mMessages)
if (hasFlag(message.flags, "\\Deleted"))
toDelete.push_back(message);
std::vector<uint32_t> adjustedSeqs;
for (size_t i = 0; i < toDelete.size(); ++i) {
uint32_t originalSeq = seq(toDelete[i].uid);
adjustedSeqs.push_back(originalSeq - (uint32_t)i);
unlink((path + "/" + toDelete[i].fileName).c_str());
}
mMessages.erase(std::remove_if(mMessages.begin(), mMessages.end(),
[&](const Message &m) { return hasFlag(m.flags, "\\Deleted"); }), mMessages.end());
save();
return adjustedSeqs;
}
}
+109
View File
@@ -0,0 +1,109 @@
#ifndef __MailFolder_h__
#define __MailFolder_h__
#include <cstdint>
#include <ctime>
#include <string>
#include <vector>
namespace mail {
///
/// MailFolder is the IMAP mailbox index for one folder directory
/// (Inbox, Spam, Trash, or any client-CREATEd folder). It does NOT
/// change how mail is delivered - SMTPServer::inCreate() still just
/// hardlinks/writes plain files straight into a folder directory, the
/// same as before IMAP existed. What MailFolder adds, alongside those
/// message files, is three small index files:
///
/// .uidvalidity - generated once, read thereafter.
/// .uidnext - monotonic counter, never reused even across
/// EXPUNGE.
/// .imapindex - one line per message: "<filename> <uid>
/// <internalDate> <flag1,flag2,...>" (last field
/// may be empty).
///
/// load() reconciles the index against an actual directory scan: any
/// message file present on disk that isn't yet in the index (freshly
/// delivered SMTP mail, most commonly) gets a UID assigned and is
/// marked \Recent for this one load() only; any indexed file no
/// longer present (expunged some other way) is dropped. This is the
/// ONLY integration point with delivery - delivery code never needs
/// to know a UID scheme exists.
///
class MailFolder {
public:
struct Message {
uint32_t uid = 0;
std::string fileName;
std::vector<std::string> flags;
bool recent = false;
size_t size = 0;
time_t internalDate = 0;
};
bool load(std::string folderPath);
void save();
uint32_t uidValidity() const { return mUidValidity; }
uint32_t uidNext() const { return mUidNext; }
size_t size() const { return mMessages.size(); }
const std::vector<Message> &messages() const { return mMessages; }
// 1-based sequence number <-> UID (ascending UID order defines
// sequence order). Returns 0 for "not present"/"out of range".
uint32_t seq(uint32_t uid) const;
uint32_t uidForSeq(uint32_t seqNum) const;
Message *find(uint32_t uid);
size_t recentCount() const;
size_t unseenCount() const;
uint32_t firstUnseenSeq() const;
std::string readMessage(uint32_t uid);
void setFlags(uint32_t uid, const std::vector<std::string> &flags);
void addFlags(uint32_t uid, const std::vector<std::string> &flags);
void removeFlags(uint32_t uid, const std::vector<std::string> &flags);
// internalDate == 0 means "use now". Not marked \Recent - an
// APPEND/COPY destination message is never \Recent (RFC 3501
// 2.3.2: \Recent means "delivered since the last time any session
// selected this mailbox", which by definition isn't true of a
// message THIS session just created).
uint32_t append(const std::string &data, const std::vector<std::string> &flags, time_t internalDate = 0);
// Unlinks every \Deleted message and removes it from the index.
// Returns the sequence numbers to report via "* n EXPUNGE", each
// ALREADY ADJUSTED for the responses sent before it (see
// __IMAP_EXPUNGE.cpp's doc comment for why that adjustment is
// required, not optional).
std::vector<uint32_t> expunge();
// Unlinks one specific message and removes it from the index -
// POP3's QUIT (RFC 1939 6) needs this instead of expunge(): POP3
// deletion marks are tracked by POP3Session as a set of message
// numbers, entirely separate from the \Deleted flag expunge()
// acts on, since POP3 has no flags at all.
void deleteMessage(uint32_t uid);
std::string path;
private:
uint32_t mUidValidity = 0;
uint32_t mUidNext = 1;
std::vector<Message> mMessages;
void loadIndexFile();
void reconcileWithDisk();
static std::string generateFileName();
};
}
#endif
+208
View File
@@ -0,0 +1,208 @@
#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;
}
}
+54
View File
@@ -0,0 +1,54 @@
#ifndef __MessageHeaders_h__
# define __MessageHeaders_h__
#include <string>
#include <vector>
namespace mail {
///
/// Text-only header parsing shared by FETCH's ENVELOPE/BODY[HEADER...]
/// data items and SEARCH's FROM/TO/CC/SUBJECT criteria. Deliberately
/// does NOT parse MIME structure (no multipart, no Content-Type
/// tree) - see MailFolder.h's own doc comment on the same scoping
/// decision (BODYSTRUCTURE is out of scope; ENVELOPE only needs
/// header text).
///
struct Address {
std::string name;
std::string mailbox;
std::string host;
};
// Splits a raw message into its header block and body, at the first
// blank line. header does NOT include the trailing blank line.
void splitMessage(const std::string &message, std::string &headerBlock, std::string &bodyBlock);
// Unfolds continuation lines (RFC 5322 3.2.2 - a line starting with
// SP/TAB continues the previous header) and returns every header as
// (name, value) in on-the-wire order. Value has the folding
// whitespace collapsed to a single space and is otherwise verbatim.
std::vector<std::pair<std::string, std::string>> parseHeaderLines(const std::string &headerBlock);
// First header matching name (case-insensitive), or "" if absent.
std::string getHeader(const std::vector<std::pair<std::string, std::string>> &headers, const std::string &name);
// "Name <user@host>, user2@host2" -> parsed addresses. Splits on
// top-level commas only (respecting "..." and <...>) - a simplified,
// not fully RFC 5322-compliant parser (no comments, no obsolete
// routing syntax), sufficient for real-world From/To/Cc header text.
std::vector<Address> parseAddressList(const std::string &headerValue);
// RFC 3501 7.4.2 ENVELOPE structure, built entirely from parsed
// headers - "sender"/"reply-to" default to "from" when their own
// header is absent, per the same RFC section's own convention.
std::string buildEnvelope(const std::string &message);
// nstring per RFC 3501 4.3 - a quoted string with backslash/quote
// escaped, or the literal NIL for an empty value.
std::string imapQuoteOrNil(const std::string &value);
}
#endif
+62 -1
View File
@@ -1,9 +1,70 @@
#include "POP3Command.h"
#include <algorithm>
namespace mail {
MailFolder::Message *pop3FindMessage(POP3Session &session, int msgNum) {
const std::vector<MailFolder::Message> &messages = session.mailbox.messages();
if (msgNum < 1 || (size_t)msgNum > messages.size())
return nullptr;
if (session.deletedMarks.count(msgNum) > 0)
return nullptr;
return session.mailbox.find(messages[msgNum - 1].uid);
}
void pop3SendMessage(POP3Session &session, const std::string &content, int maxBodyLines) {
size_t pos = 0;
bool inBody = false;
int bodyLinesSent = 0;
while (pos <= content.size()) {
size_t next = content.find("\r\n", pos);
std::string line = (next == std::string::npos) ? content.substr(pos) : content.substr(pos, next - pos);
bool isBlank = line.empty();
if (inBody && maxBodyLines >= 0 && bodyLinesSent >= maxBodyLines)
break;
if (!line.empty() && line[0] == '.')
session.out << "." << line << CRLF;
else
session.out << line << CRLF;
if (inBody)
++bodyLinesSent;
if (isBlank && !inBody)
inBody = true;
if (next == std::string::npos)
break;
pos = next + 2;
}
session.out << "." << CRLF;
}
int POP3Command::processCommand(coreutils::ZString &request, core::TCPSession &session) {
return processCommand(request, (POP3Session &)session, (POP3Server &)session.server);
// Same bug, same fix as IMAPCommand::processCommand's own doc
// comment: session.server is a TCPServer* pointer, and casting
// the pointer VALUE directly to a reference type reinterprets the
// pointer FIELD'S OWN memory location as the object, not what it
// points to. Dereference first.
// ZString's copy constructor (ZString.cpp) copies data/length/
// cursor but NOT `list` - and the specific-handler overload just
// below takes request BY VALUE, so every POP3 handler's `request`
// is such a copy. request[N] (backed by `list`, populated by
// CommandList::processRequest()'s own request.split() call before
// dispatch) would therefore index into an empty vector - out of
// bounds, the same class of bug as MailFileSystem::getMailBoxPath()
// had for mailbox[1] before this session's IMAP work fixed it.
// POP3 handlers must read arguments off the (correctly-copied)
// cursor instead, via getTokenExclude()/skipWhitespace() - which
// is also why the command word needs stripping here first, same
// reasoning as IMAPCommand::processCommand's own doc comment:
// request.reset() (CommandList::processRequest) leaves the cursor
// at the true start of the line, still including the command
// word itself.
request.getTokenExclude(" ");
return processCommand(request, (POP3Session &)session, (POP3Server &)(*session.server));
}
int POP3Command::processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) {
+19
View File
@@ -8,6 +8,25 @@ namespace mail {
class POP3Server;
// RFC 1939 3: POP3 message numbers are 1-based positions in the
// maildrop's fixed-for-the-session ordering - session.mailbox's own
// ascending-by-UID message list, loaded once at PASS and never
// reloaded mid-session, already IS that fixed ordering, so message
// number N is simply session.mailbox.messages()[N-1]. Returns
// nullptr for an out-of-range number OR a number already marked
// \Deleted this session (RFC 1939 3: "the POP3 server MUST NOT" let
// a deleted message be referenced by any command).
MailFolder::Message *pop3FindMessage(POP3Session &session, int msgNum);
// Shared by RETR and TOP - RFC 1939 3's byte-stuffing rule (any line
// starting with "." gets an extra "." prepended, same transparency
// rule as SMTP DATA, RFC 5321 4.5.2) applies to both, and TOP is
// otherwise identical to RETR except it stops after maxBodyLines
// lines of body (headers and the header/body blank line always go
// out in full either way). maxBodyLines < 0 means unlimited (RETR's
// own call site).
void pop3SendMessage(POP3Session &session, const std::string &content, int maxBodyLines);
class POP3Command : public core::Command {
public:
+22 -2
View File
@@ -2,7 +2,11 @@
#define __POP3Server_h__
#include "TCPServer.h"
#include "TLSInfo.h"
#include "Exception.h"
#include "MailFileSystem.h"
#include "POP3Session.h"
#include "__POP3_CAPA.h"
#include "__POP3_DELE.h"
#include "__POP3_LAST.h"
#include "__POP3_LIST.h"
@@ -14,6 +18,7 @@
#include "__POP3_RSET.h"
#include "__POP3_STAT.h"
#include "__POP3_TOP.h"
#include "__POP3_UIDL.h"
#include "__POP3_USER.h"
namespace mail {
@@ -21,8 +26,11 @@ namespace mail {
class POP3Server : public core::TCPServer {
public:
POP3Server(core::EPoll &epoll, std::string hostName, MailFileSystem &mailFileSystem, core::IPAddress ipAddress)
: TCPServer(epoll, ipAddress, " ", 10, "POP3 Server"), mailFileSystem(mailFileSystem), hostName(hostName) {
POP3Server(core::EPoll &epoll, std::string hostName, MailFileSystem &mailFileSystem,
core::IPAddress ipAddress, core::TLSInfo *tlsInfo)
: TCPServer(epoll, ipAddress, tlsInfo, " ", 10, "POP3 Server"), mailFileSystem(mailFileSystem), hostName(hostName) {
// RFC 2449: was not registered at all before.
commands.add(_pop3_capa, "CAPA");
commands.add(_pop3_dele, "DELE");
commands.add(_pop3_last, "LAST");
commands.add(_pop3_list, "LIST");
@@ -34,13 +42,24 @@ namespace mail {
commands.add(_pop3_rset, "RSET");
commands.add(_pop3_stat, "STAT");
commands.add(_pop3_top, "TOP");
commands.add(_pop3_uidl, "UIDL");
commands.add(_pop3_user, "USER");
}
MailFileSystem &mailFileSystem;
std::string hostName;
// Same reasoning as IMAPServer's own override - without it every
// accepted connection gets a plain TCPSession, POP3Session's
// greeting never fires, and POP3Command's (POP3Session &)session
// casts are undefined behavior against an object that was never
// actually a POP3Session.
POP3Session *getSocketAccept(core::EPoll &ePoll) override {
return new POP3Session(ePoll, *this);
}
private:
__POP3_CAPA _pop3_capa;
__POP3_DELE _pop3_dele;
__POP3_LAST _pop3_last;
__POP3_LIST _pop3_list;
@@ -52,6 +71,7 @@ namespace mail {
__POP3_RSET _pop3_rset;
__POP3_STAT _pop3_stat;
__POP3_TOP _pop3_top;
__POP3_UIDL _pop3_uidl;
__POP3_USER _pop3_user;
};
+40 -6
View File
@@ -1,19 +1,53 @@
#ifndef __POP3Session_h__
#define __POP3Session_h__
#include "TCPServer.h"
#include "TCPSession.h"
#include "MailFolder.h"
#include <set>
#define CRLF "\r\n"
namespace mail {
class POP3Session : public core::TCPSession {
enum AuthState {
USER_UNKNOWN, USER_QUERY, USER_SECRET_QUERY, USER_KNOWN
};
public:
POP3Session(core::EPoll &ePoll, core::TCPServer &server)
: TCPSession(ePoll, &server, "POP3 Client Session") {}
std::string clientDomainName;
// RFC 1939 4/7: USER sets the pending username and replies +OK
// without checking anything - only PASS actually authenticates
// (against MailFileSystem::checkPassword(), shared with SMTP AUTH
// and IMAP LOGIN). userName is meaningless until PASS succeeds;
// authenticated gates every other command per RFC 1939 3's state
// diagram (AUTHORIZATION vs TRANSACTION state).
std::string userName;
std::string password;
AuthState authstate = USER_UNKNOWN;
bool relay = false;
bool authenticated = false;
// Loaded once, right after a successful PASS - POP3 has no
// folder concept (RFC 1939 has no SELECT-equivalent), it only
// ever operates on Inbox. RFC 1939 3's "maildrop" message
// numbering is fixed for the whole session (assigned once here,
// not re-derived per command) even if new mail is delivered
// mid-session - deletedMarks/QUIT below follow the same
// session-frozen-snapshot model.
MailFolder mailbox;
// RFC 1939 3: DELE only MARKS a message deleted - the mark (and
// any RSET undoing it) is session-local and has no effect until
// QUIT actually commits it by unlinking the marked messages.
// Keyed by 1-based POP3 message number (this session's frozen
// ordering), not UID.
std::set<int> deletedMarks;
// RFC 1939 4: the server must send a positive greeting before the
// client sends anything.
void onConnected() override {
out << "+OK POP3 server ready" << CRLF;
}
};
+12 -3
View File
@@ -3,7 +3,16 @@
namespace mail {
int SMTPCommand::processCommand(coreutils::ZString &request, core::TCPSession &session) {
return processCommand(request, (SMTPSession &)session, (SMTPServer &)session.server);
// Same bug, same fix as IMAPCommand::processCommand's own doc
// comment: session.server is a TCPServer* pointer, and casting
// the pointer VALUE directly to a reference type reinterprets the
// pointer FIELD'S OWN memory location as the object, not what it
// points to. Confirmed live via gdb - this is the actual cause of
// a real SIGSEGV in __SMTP_EHLO's very first use of
// server.hostName (and every other handler reading
// server.mailFileSystem was silently reading the same garbage,
// just without happening to crash on it yet). Dereference first.
return processCommand(request, (SMTPSession &)session, (SMTPServer &)(*session.server));
}
int SMTPCommand::processCommand(coreutils::ZString &request, SMTPSession &session, SMTPServer &server) {
@@ -11,11 +20,11 @@ namespace mail {
}
void SMTPCommand::grabInput(SMTPSession &session) {
session.server.commands.grabInput(session, *this);
session.server->commands.grabInput(session, *this);
}
void SMTPCommand::clearGrab(SMTPSession &session) {
session.server.commands.clearGrab(session);
session.server->commands.clearGrab(session);
}
}
+50 -7
View File
@@ -1,24 +1,33 @@
#include "SMTPServer.h"
#include "Log.h"
#include "SendMail.h"
#include "SpamAssassinFilter.h"
#include "File.h"
#include <thread>
namespace mail {
SMTPServer::SMTPServer(core::EPoll &ePoll, std::string hostName, MailFileSystem &mailFileSystem, core::IPAddress ipAddress)
: core::TCPServer(ePoll, ipAddress, " ", 1, "SMTP Server"), core::INotify(ePoll), ePoll(ePoll), hostName(hostName), mailFileSystem(mailFileSystem) {
SMTPServer::SMTPServer(core::EPoll &ePoll, std::string hostName, MailFileSystem &mailFileSystem,
core::IPAddress ipAddress, core::TLSInfo *tlsInfo, bool watchQueue,
DKIMSigner *dkimSigner)
: core::TCPServer(ePoll, ipAddress, tlsInfo, " ", 1, "SMTP Server"), core::INotify(ePoll), ePoll(ePoll), hostName(hostName), mailFileSystem(mailFileSystem), dkimSigner(dkimSigner) {
commands.add(_smtp_auth, "AUTH");
commands.add(_smtp_data, "DATA");
commands.add(_smtp_ehlo, "EHLO");
commands.add(_smtp_expn, "EXPN");
commands.add(_smtp_helo, "HELO");
commands.add(_smtp_help, "HELP");
commands.add(_smtp_mail, "MAIL");
commands.add(_smtp_noop, "NOOP");
commands.add(_smtp_quit, "QUIT");
commands.add(_smtp_rcpt, "RCPT");
commands.add(_smtp_rset, "RSET");
commands.add(_smtp_vrfy, "VRFY");
if (watchQueue) {
processExisting();
wd = addWatch(mailFileSystem.getQueuePath());
}
}
SMTPSession * SMTPServer::getSocketAccept(core::EPoll &ePoll) {
return new SMTPSession(ePoll, *this);
@@ -31,18 +40,52 @@ namespace mail {
void SMTPServer::inCreate(coreutils::ZString &name) {
coreutils::Log(coreutils::LOG_DEBUG_2) << "inCreate; [" << name << "]";
name.split("|");
if(name.getList().size() > 1) {
// Three parts now: messageid|recipient|sender - see
// __SMTP_DATA::queueMail()'s own doc comment for why the sender
// had to be added alongside the recipient.
if(name.getList().size() > 2) {
bool exists = mailFileSystem.ifMailBoxExists(name[1]);
coreutils::MString path;
path << mailFileSystem.getMailBoxPath(name[1]) << "/Inbox/" << name[0];
coreutils::MString from;
from << mailFileSystem.getQueuePath() << "/" << name;
if(exists) {
int rc = link(from.c_str(), path.c_str());
rc = unlink(from.c_str());
coreutils::Log(coreutils::LOG_INFO) << "Message " << name[0] << " delivered to recipient " << name[1] << ".";
// Scanned on the way into the recipient's own Inbox, not the
// outbound relay path below - spamd's added X-Spam-* headers
// are for whatever reads this recipient's mailbox, and a
// message this server relays onward gets scored by ITS
// destination's own filtering, same as any other MTA. Reading
// the whole file in (rather than the plain link() this branch
// used to do) is unavoidable now that spamd may have rewritten
// the content the recipient actually needs to receive.
coreutils::File queuedFile(from, O_RDONLY);
queuedFile.read();
SpamAssassinFilter::Result scan = SpamAssassinFilter::scan(queuedFile.asString());
coreutils::File mailFile(path, O_CREAT | O_WRONLY, 0660);
mailFile.write(scan.scanned);
int rc = unlink(from.c_str());
coreutils::Log(coreutils::LOG_INFO) << "Message " << name[0] << " delivered to recipient " << name[1]
<< (scan.ok && scan.isSpam ? " (flagged as spam)" : "") << ".";
} else {
mailQueue.emplace_back(ePoll, name[1].str(), name[1].str(), from.str());
// Not a local mailbox - relay it out over SMTP. SendMail's
// own send() is a plain blocking call (see its doc comment
// for why) - queued delivery wants fire-and-forget, so this
// owns the heap-allocated SendMail itself and runs it on its
// own detached thread rather than blocking this EPoll
// worker thread for however long the remote host takes.
coreutils::MString sender(name[2]);
coreutils::MString recipient(name[1]);
coreutils::File queuedFile(from, O_RDONLY);
queuedFile.read();
coreutils::MString mailData(queuedFile.asString());
SendMail *outbound = new SendMail(hostName, mailFileSystem.getSendLogPath().str(),
name[0].str(), sender, recipient, mailData, dkimSigner);
std::thread([outbound]() {
outbound->send();
delete outbound;
}).detach();
}
}
}
+27 -2
View File
@@ -6,12 +6,15 @@
# include <filesystem>
# include "EPoll.h"
# include "TCPServer.h"
# include "TLSInfo.h"
# include "SMTPSession.h"
# include "MailFileSystem.h"
# include "__SMTP_AUTH.h"
# include "__SMTP_DATA.h"
# include "__SMTP_EHLO.h"
# include "__SMTP_EXPN.h"
# include "__SMTP_HELO.h"
# include "__SMTP_HELP.h"
# include "__SMTP_MAIL.h"
# include "__SMTP_NOOP.h"
# include "__SMTP_QUIT.h"
@@ -20,16 +23,37 @@
# include "__SMTP_VRFY.h"
# include "INotify.h"
# include "SendMail.h"
# include "DKIMSigner.h"
namespace mail {
class SMTPServer : public core::TCPServer, public core::INotify {
public:
SMTPServer(core::EPoll &ePoll, std::string hostName, MailFileSystem &mailFileSystem, core::IPAddress ipAddress);
// watchQueue controls whether this instance watches the mail
// queue directory and relays/delivers what lands in it - true by
// default for the ordinary single-listener case. Running a
// second SMTPServer for a submission/TLS port alongside the
// plaintext one needs watchQueue=false on exactly one of the two:
// each SMTPServer's own INotify watch is independent, so two
// watchers on the same directory would each fire inCreate() for
// every queued message, delivering it twice.
// dkimSigner is optional (nullptr = outbound mail goes out
// unsigned) and not owned - passed straight through to every
// SendMail this instance's inCreate() constructs for outbound
// relay.
SMTPServer(core::EPoll &ePoll, std::string hostName, MailFileSystem &mailFileSystem,
core::IPAddress ipAddress, core::TLSInfo *tlsInfo, bool watchQueue = true,
DKIMSigner *dkimSigner = nullptr);
MailFileSystem &mailFileSystem;
std::string hostName;
core::EPoll &ePoll;
DKIMSigner *dkimSigner;
// Advertised via EHLO's "250-SIZE <n>" and enforced by __SMTP_DATA
// while accumulating a message body - RFC 1870. 25MB, matching
// the limit most real providers (e.g. Gmail) use.
static const size_t maxMessageSize = 25 * 1024 * 1024;
SMTPSession * getSocketAccept(core::EPoll &ePoll) override;
void sessionErrorHandler(std::string errorString, std::stringstream &out);
@@ -41,7 +65,9 @@ namespace mail {
__SMTP_AUTH _smtp_auth;
__SMTP_DATA _smtp_data;
__SMTP_EHLO _smtp_ehlo;
__SMTP_EXPN _smtp_expn;
__SMTP_HELO _smtp_helo;
__SMTP_HELP _smtp_help;
__SMTP_MAIL _smtp_mail;
__SMTP_NOOP _smtp_noop;
__SMTP_QUIT _smtp_quit;
@@ -49,7 +75,6 @@ namespace mail {
__SMTP_RSET _smtp_rset;
__SMTP_VRFY _smtp_vrfy;
int wd;
std::vector<SendMail> mailQueue;
void processExisting();
+6 -2
View File
@@ -1,14 +1,18 @@
# include "SMTPSession.h"
# include "SMTPServer.h"
# include "Log.h"
namespace mail {
SMTPSession::SMTPSession(core::EPoll &ePoll, core::TCPServer &server)
: TCPSession(ePoll, server, "SMTP Client Session") {}
: TCPSession(ePoll, &server, "SMTP Client Session") {}
void SMTPSession::onConnected() {
coreutils::Log(coreutils::LOG_DEBUG_2) << "onConnected -> sending 220";
out << "220 localhost BMAMail" << CRLF;
// RFC 5321 4.2 requires the greeting to name the server's own
// domain - the literal "localhost" was hardcoded here regardless
// of the hostName this server was actually configured with.
out << "220 " << ((SMTPServer *)server)->hostName << " BMAMail" << CRLF;
}
}
+19 -1
View File
@@ -4,6 +4,7 @@
# include "TCPSession.h"
# include "TCPServer.h"
# include "MString.h"
# include "SPFChecker.h"
# define CRLF "\r\n"
@@ -27,10 +28,27 @@ namespace mail {
State state = CONNECT;
AuthState authState = USER_UNKNOWN;
Mode mode = WAIT_FOR_DATA;
bool relay = true;
// Defaults to false: relaying to a non-local recipient is only
// permitted after a real, successful AUTH (see __SMTP_AUTH.cpp,
// which sets this true on success) - defaulting to true made every
// brand-new, unauthenticated connection an open relay for anyone
// (__SMTP_RCPT.cpp checks exactly this flag before accepting a
// recipient whose mailbox does not exist locally).
bool relay = false;
coreutils::MString from;
std::vector<coreutils::MString> recipientList;
// Computed once at MAIL FROM time (see __SMTP_MAIL.cpp) and
// reused at DATA time (__SMTP_DATA.cpp), rather than re-running
// the SPF DNS lookups a second time for the same transaction. The
// header is ready to prepend to the message as-is; domain/result
// are also kept separately since DMARC's own alignment check
// (also run at DATA time) needs the raw identity/result, not the
// pre-formatted header text.
std::string spfHeader;
std::string spfDomain;
SPFChecker::Result spfResult = SPFChecker::Result::None;
};
}
+357
View File
@@ -0,0 +1,357 @@
#include "SPFChecker.h"
#include <algorithm>
#include <arpa/inet.h>
#include <arpa/nameser.h>
#include <cctype>
#include <cstring>
#include <netdb.h>
#include <resolv.h>
#include <sstream>
namespace mail {
namespace {
struct Term {
char qualifier; // '+', '-', '~', '?'
std::string name; // "all", "ip4", "a", "mx", "include", "exists", "redirect", "exp", or an unknown modifier's key
std::string value; // everything after ':'/'=' (domain-spec, ip/cidr, etc.) - empty if none
bool isModifier; // "redirect=" / "exp=" / unknown "key=value" vs. an actual mechanism
};
std::vector<Term> parseTerms(const std::string &record) {
std::vector<Term> terms;
std::istringstream stream(record);
std::string token;
bool first = true;
while (stream >> token) {
if (first) {
first = false;
continue; // "v=spf1" itself
}
Term t;
t.qualifier = '+';
std::string body = token;
if (!body.empty() && (body[0] == '+' || body[0] == '-' || body[0] == '~' || body[0] == '?')) {
t.qualifier = body[0];
body = body.substr(1);
}
size_t eq = body.find('=');
size_t colon = body.find(':');
if (eq != std::string::npos && (colon == std::string::npos || eq < colon)) {
t.isModifier = true;
t.name = body.substr(0, eq);
t.value = body.substr(eq + 1);
} else {
t.isModifier = false;
size_t sep = body.find_first_of(":/");
if (sep == std::string::npos) {
t.name = body;
t.value = "";
} else {
t.name = body.substr(0, sep);
t.value = body.substr(body[sep] == ':' ? sep + 1 : sep); // keep leading '/' for cidr-only forms
}
}
for (char &c : t.name)
c = std::tolower((unsigned char)c);
terms.push_back(t);
}
return terms;
}
// Splits "domain[/v4prefix[/v6prefix]]" (the tail of a/mx after an
// optional ":domain") into its domain part (possibly empty, meaning
// "use the checked domain") and the two optional prefix lengths
// (-1 = "not given", meaning "match the full address").
void splitDomainAndCidr(const std::string &value, std::string &domain, int &v4prefix, int &v6prefix) {
domain = value;
v4prefix = -1;
v6prefix = -1;
size_t slash = domain.find('/');
if (slash == std::string::npos)
return;
std::string cidrPart = domain.substr(slash + 1);
domain = domain.substr(0, slash);
size_t slash2 = cidrPart.find('/');
std::string v4 = (slash2 == std::string::npos) ? cidrPart : cidrPart.substr(0, slash2);
std::string v6 = (slash2 == std::string::npos) ? "" : cidrPart.substr(slash2 + 1);
if (!v4.empty())
v4prefix = atoi(v4.c_str());
if (!v6.empty())
v6prefix = atoi(v6.c_str());
}
SPFChecker::Result qualifierToResult(char q) {
switch (q) {
case '-': return SPFChecker::Result::Fail;
case '~': return SPFChecker::Result::SoftFail;
case '?': return SPFChecker::Result::Neutral;
default: return SPFChecker::Result::Pass;
}
}
}
std::vector<std::string> SPFChecker::lookupSPFRecords(const std::string &domain) {
std::vector<std::string> records;
unsigned char answer[4096];
int len = res_query(domain.c_str(), C_IN, T_TXT, answer, sizeof(answer));
if (len <= 0)
return records;
ns_msg handle;
if (ns_initparse(answer, len, &handle) != 0)
return records;
int count = ns_msg_count(handle, ns_s_an);
for (int i = 0; i < count; ++i) {
ns_rr rr;
if (ns_parserr(&handle, ns_s_an, i, &rr) != 0 || ns_rr_type(rr) != ns_t_txt)
continue;
// RFC 7208 3.3: a TXT record's RDATA is one or more
// length-prefixed character-strings, concatenated here into
// one logical string before checking for the "v=spf1 " prefix.
const unsigned char *rdata = ns_rr_rdata(rr);
int rdlen = ns_rr_rdlen(rr);
std::string full;
int pos = 0;
while (pos < rdlen) {
int segLen = rdata[pos];
++pos;
if (pos + segLen > rdlen)
break;
full.append((const char *)(rdata + pos), segLen);
pos += segLen;
}
std::string lower = full;
for (char &c : lower)
c = std::tolower((unsigned char)c);
if (lower.rfind("v=spf1", 0) == 0 && (lower.size() == 6 || lower[6] == ' '))
records.push_back(full);
}
return records;
}
std::vector<std::string> SPFChecker::lookupA(const std::string &domain) {
std::vector<std::string> ips;
struct addrinfo hints {};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo *result = nullptr;
if (getaddrinfo(domain.c_str(), nullptr, &hints, &result) != 0 || !result)
return ips;
for (struct addrinfo *rp = result; rp != nullptr; rp = rp->ai_next) {
char buf[INET6_ADDRSTRLEN];
void *addr = &((struct sockaddr_in *)rp->ai_addr)->sin_addr;
if (inet_ntop(AF_INET, addr, buf, sizeof(buf)))
ips.push_back(buf);
}
freeaddrinfo(result);
return ips;
}
std::vector<std::string> SPFChecker::lookupMX(const std::string &domain) {
std::vector<std::string> hosts;
unsigned char answer[4096];
int len = res_query(domain.c_str(), C_IN, T_MX, answer, sizeof(answer));
if (len <= 0)
return hosts;
ns_msg handle;
if (ns_initparse(answer, len, &handle) != 0)
return hosts;
int count = ns_msg_count(handle, ns_s_an);
// RFC 7208 4.6.4: processing more than 10 MX names per mx
// mechanism is itself a PermError condition - capped here rather
// than trusting a (possibly hostile) record to stay reasonable.
for (int i = 0; i < count && i < 10; ++i) {
ns_rr rr;
if (ns_parserr(&handle, ns_s_an, i, &rr) != 0 || ns_rr_type(rr) != ns_t_mx)
continue;
char host[NS_MAXDNAME];
if (dn_expand(ns_msg_base(handle), ns_msg_end(handle), ns_rr_rdata(rr) + 2, host, sizeof(host)) >= 0)
hosts.push_back(host);
}
return hosts;
}
bool SPFChecker::ipInCIDR(const std::string &ip, const std::string &cidrSpec, int defaultPrefixV4, int defaultPrefixV6) {
bool isV6 = ip.find(':') != std::string::npos;
if (isV6) {
struct in6_addr a{}, n{};
if (inet_pton(AF_INET6, ip.c_str(), &a) != 1 || inet_pton(AF_INET6, cidrSpec.c_str(), &n) != 1)
return false;
int prefix = (defaultPrefixV6 >= 0) ? defaultPrefixV6 : 128;
if (prefix < 0 || prefix > 128)
return false;
int fullBytes = prefix / 8;
int remBits = prefix % 8;
if (memcmp(a.s6_addr, n.s6_addr, fullBytes) != 0)
return false;
if (remBits > 0) {
unsigned char mask = (unsigned char)(0xFF << (8 - remBits));
if ((a.s6_addr[fullBytes] & mask) != (n.s6_addr[fullBytes] & mask))
return false;
}
return true;
} else {
struct in_addr a{}, n{};
if (inet_pton(AF_INET, ip.c_str(), &a) != 1 || inet_pton(AF_INET, cidrSpec.c_str(), &n) != 1)
return false;
int prefix = (defaultPrefixV4 >= 0) ? defaultPrefixV4 : 32;
if (prefix < 0 || prefix > 32)
return false;
uint32_t mask = (prefix == 0) ? 0 : htonl(0xFFFFFFFFu << (32 - prefix));
return (a.s_addr & mask) == (n.s_addr & mask);
}
}
SPFChecker::Result SPFChecker::checkRecursive(const std::string &domain, const std::string &clientIP, int &mechanismCount) {
std::vector<std::string> records = lookupSPFRecords(domain);
if (records.empty())
return Result::None;
if (records.size() > 1)
return Result::PermError; // RFC 7208 4.5: more than one v=spf1 record is a PermError
std::vector<Term> terms = parseTerms(records[0]);
std::string redirectTarget;
for (auto &t : terms) {
if (t.isModifier) {
if (t.name == "redirect")
redirectTarget = t.value;
continue; // "exp" and unrecognized modifiers are not evaluated
}
bool matched = false;
if (t.name == "all") {
matched = true;
} else if (t.name == "ip4" || t.name == "ip6") {
std::string cidr;
int v4p = -1, v6p = -1;
splitDomainAndCidr(t.value, cidr, v4p, v6p);
// ip4/ip6 use a single trailing /prefix, not the dual form -
// splitDomainAndCidr's "second slash" case does not apply
// here, so whichever of v4p/v6p got set is the one that
// matters for this term's own family.
int prefix = (t.name == "ip4") ? v4p : v6p;
matched = ipInCIDR(clientIP, cidr, t.name == "ip4" ? prefix : -1, t.name == "ip6" ? prefix : -1);
} else if (t.name == "a" || t.name == "mx") {
if (t.value.find('%') != std::string::npos)
continue; // macro - not supported, skip (see class doc comment)
std::string targetDomain;
int v4p = -1, v6p = -1;
splitDomainAndCidr(t.value, targetDomain, v4p, v6p);
if (targetDomain.empty())
targetDomain = domain;
std::vector<std::string> candidateDomains;
if (t.name == "a") {
candidateDomains.push_back(targetDomain);
} else {
if (++mechanismCount > 10)
return Result::PermError;
candidateDomains = lookupMX(targetDomain);
}
for (auto &cd : candidateDomains) {
if (++mechanismCount > 10)
return Result::PermError;
for (auto &ip : lookupA(cd)) {
if (ipInCIDR(clientIP, ip, v4p, v6p)) {
matched = true;
break;
}
}
if (matched)
break;
}
} else if (t.name == "exists") {
if (t.value.find('%') != std::string::npos)
continue; // macro - not supported, skip
if (++mechanismCount > 10)
return Result::PermError;
matched = !lookupA(t.value).empty();
} else if (t.name == "include") {
if (++mechanismCount > 10)
return Result::PermError;
Result sub = checkRecursive(t.value, clientIP, mechanismCount);
// RFC 7208 5.2: only a Pass from the included domain makes
// "include" itself match; Fail/SoftFail/Neutral there just
// mean "this mechanism didn't match", not that the whole
// record fails - but an error in the sub-check is a real
// error here too, not silently swallowed.
if (sub == Result::PermError || sub == Result::TempError)
return sub;
matched = (sub == Result::Pass);
} else if (t.name == "ptr") {
continue; // deprecated, unsupported - see class doc comment
} else {
// Unrecognized mechanism name - RFC 7208 5: PermError.
return Result::PermError;
}
if (matched)
return qualifierToResult(t.qualifier);
}
if (!redirectTarget.empty()) {
if (++mechanismCount > 10)
return Result::PermError;
return checkRecursive(redirectTarget, clientIP, mechanismCount);
}
// RFC 7208 4.7: no mechanism matched and no redirect - Neutral,
// as if "?all" had been appended.
return Result::Neutral;
}
SPFChecker::Result SPFChecker::check(const std::string &domain, const std::string &clientIP) {
if (domain.empty())
return Result::None;
int mechanismCount = 0;
return checkRecursive(domain, clientIP, mechanismCount);
}
std::string SPFChecker::resultName(Result result) {
switch (result) {
case Result::Pass: return "pass";
case Result::Fail: return "fail";
case Result::SoftFail: return "softfail";
case Result::Neutral: return "neutral";
case Result::None: return "none";
case Result::PermError: return "permerror";
case Result::TempError: return "temperror";
}
return "none";
}
std::string SPFChecker::receivedSPFHeader(Result result, const std::string &domain,
const std::string &clientIP, const std::string &helo,
const std::string &receivingHost) {
std::string name = resultName(result);
std::string explanation;
switch (result) {
case Result::Pass: explanation = "domain of " + domain + " designates " + clientIP + " as permitted sender"; break;
case Result::Fail: explanation = "domain of " + domain + " does not designate " + clientIP + " as permitted sender"; break;
case Result::SoftFail: explanation = "domain of " + domain + " does not designate " + clientIP + " as permitted sender (soft fail)"; break;
case Result::Neutral: explanation = "domain of " + domain + " has no definitive policy for " + clientIP; break;
case Result::None: explanation = "domain of " + domain + " does not designate permitted sender hosts"; break;
case Result::PermError: explanation = "permanent error in processing domain of " + domain; break;
case Result::TempError: explanation = "temporary error in processing domain of " + domain; break;
}
return "Received-SPF: " + name + " (" + receivingHost + ": " + explanation + ") client-ip=" + clientIP +
"; envelope-from=" + domain + "; helo=" + helo + ";\r\n";
}
}
+71
View File
@@ -0,0 +1,71 @@
#ifndef __SPFChecker_h__
#define __SPFChecker_h__
#include <string>
#include <vector>
namespace mail {
///
/// SPFChecker evaluates RFC 7208 Sender Policy Framework: whether
/// the IP address a connection actually came from is authorized to
/// send mail claiming to be from a given domain.
///
/// Deliberately does not implement two parts of the RFC: macro
/// expansion (7208 7) and the ptr mechanism (7208 5.5) - macros are
/// rare outside large/complex senders, and the RFC itself says ptr
/// "SHOULD NOT be published" and "SHOULD be used as a last resort"
/// by checkers, precisely because it is unreliable and expensive (a
/// reverse-then-forward DNS round trip per candidate name). A
/// mechanism using either is simply skipped (treated as
/// non-matching) rather than failing the whole check.
///
class SPFChecker {
public:
enum class Result { Pass, Fail, SoftFail, Neutral, None, PermError, TempError };
///
/// domain is the identity being checked (the envelope MAIL FROM's
/// domain, or the HELO/EHLO domain when MAIL FROM is the null
/// reverse-path <> - RFC 7208 2.3/2.4). clientIP is the IP
/// address of the SMTP client that actually connected.
///
static Result check(const std::string &domain, const std::string &clientIP);
static std::string resultName(Result result);
///
/// A standard Received-SPF: header (RFC 7208 9.1, the convention
/// essentially every inbound MTA follows) recording the result -
/// downstream processing (a user's mail client, a later
/// spam-scoring pass) can key off this without redoing the DNS
/// work itself.
///
static std::string receivedSPFHeader(Result result, const std::string &domain,
const std::string &clientIP, const std::string &helo,
const std::string &receivingHost);
private:
// mechanismCount is threaded by reference through every
// recursive/redirected call so the RFC 7208 4.6.4 limit (10
// DNS-querying terms total across the whole evaluation, not per
// record) is enforced globally, not reset at each level.
static Result checkRecursive(const std::string &domain, const std::string &clientIP, int &mechanismCount);
static std::vector<std::string> lookupSPFRecords(const std::string &domain);
static std::vector<std::string> lookupA(const std::string &domain);
static std::vector<std::string> lookupMX(const std::string &domain);
static bool ipInCIDR(const std::string &ip, const std::string &cidrSpec, int defaultPrefixV4, int defaultPrefixV6);
};
}
#endif
+226 -106
View File
@@ -1,130 +1,250 @@
#include "SendMail.h"
#include <chrono>
#include <sys/types.h>
#include <netinet/in.h>
#include "Log.h"
#include <algorithm>
#include <arpa/inet.h>
#include <arpa/nameser.h>
#include <cstdlib>
#include <netdb.h>
#include <netinet/in.h>
#include <resolv.h>
#include <sstream>
#include <sys/socket.h>
#include <unistd.h>
#include <utility>
namespace mail {
SendMail::SendMail(core::EPoll &ePoll, std::string from, std::string recipient, std::string mailFileName)
: core::TCPSession2(ePoll, "Send Mail Agent"), from(from), recipient(recipient), mailFileName(mailFileName),
mailData(mailFileName), Timer(ePoll, 0.00f), ePoll(ePoll) {
SendMail::SendMail(std::string hostName, std::string sendLogPath, std::string messageId,
coreutils::MString from, coreutils::MString recipient, coreutils::MString mailData,
DKIMSigner *dkimSigner)
: hostName(hostName), sendLogPath(sendLogPath), messageId(messageId),
from(from), recipient(recipient), mailData(mailData), dkimSigner(dkimSigner) {}
coreutils::Log(coreutils::LOG_DEBUG_1) << this->recipient;
this->recipient.split("@");
coreutils::Log(coreutils::LOG_DEBUG_1) << this->recipient[1];
std::string SendMail::domainOf(coreutils::MString &address) {
std::string addr = address.str();
size_t at = addr.find_last_of('@');
return (at == std::string::npos) ? addr : addr.substr(at + 1);
}
u_char nsbuff[4096];
char *domain = this->recipient[1].c_str();
int len = res_query(domain, ns_c_any, ns_t_mx, nsbuff, sizeof(nsbuff));
coreutils::Log(coreutils::LOG_DEBUG_1) << ">" << len;
ns_msg msg;
ns_initparse(nsbuff, len, &msg);
len = ns_msg_count(msg, ns_s_an);
char dispbuf[4096];
std::vector<std::string> SendMail::lookupMX(const std::string &domain) {
std::vector<std::pair<int, std::string>> records;
unsigned char answer[4096];
int len = res_query(domain.c_str(), C_IN, T_MX, answer, sizeof(answer));
if (len > 0) {
ns_msg handle;
if (ns_initparse(answer, len, &handle) == 0) {
int count = ns_msg_count(handle, ns_s_an);
for (int i = 0; i < count; ++i) {
ns_rr rr;
for (int i = 0; i < 1; i++) {
ns_parserr(&msg, ns_s_an, i, &rr);
ns_sprintrr(&msg, &rr, NULL, NULL, dispbuf, sizeof(dispbuf));
printf ("%s\n", dispbuf);
if (ns_parserr(&handle, ns_s_an, i, &rr) == 0 && ns_rr_type(rr) == ns_t_mx) {
const unsigned char *rdata = ns_rr_rdata(rr);
int preference = (rdata[0] << 8) | rdata[1];
char host[NS_MAXDNAME];
if (dn_expand(ns_msg_base(handle), ns_msg_end(handle), rdata + 2, host, sizeof(host)) >= 0)
records.emplace_back(preference, std::string(host));
}
}
}
}
std::sort(records.begin(), records.end(),
[](const std::pair<int, std::string> &a, const std::pair<int, std::string> &b) {
return a.first < b.first;
});
std::vector<std::string> hosts;
for (auto &r : records)
hosts.push_back(r.second);
// RFC 5321 5.1: a domain with no MX records is treated as a
// single implicit MX naming that domain itself.
if (hosts.empty())
hosts.push_back(domain);
return hosts;
}
coreutils::ZString mxArray(dispbuf);
mxArray.split("\t");
mxArray[3].split(" ");
coreutils::Log(coreutils::LOG_DEBUG_1) << "MX: " << mxArray[3][1];
core::IPAddress mxAddress(mxArray[3][1].c_str(), 25);
connect(mxAddress);
coreutils::Log(coreutils::LOG_DEBUG_1) << "SendMail initiated...";
}
SendMail::SendMail(const SendMail &copy) : core::TCPSession2(ePoll, "Send Mail Agent"), from(copy.from), recipient(copy.recipient), mailFileName(copy.mailFileName),
mailData(mailFileName), Timer(ePoll, 0.00f), ePoll(copy.ePoll) {}
SendMail::~SendMail() {
coreutils::Log(coreutils::LOG_DEBUG_1) << "SendMail destructing...";
}
void SendMail::onTimeout() {
state = stateOnTimeout;
coreutils::Log(coreutils::LOG_DEBUG_1) << "onTimeout...";
protocol(buffer);
TCPSession2::send();
}
void SendMail::protocol(coreutils::ZString &data) {
coreutils::Log(coreutils::LOG_DEBUG_1) << "[" << data << "..." << state;
switch (state) {
case CONNECT:
// read recipient
// parse email for name and domain name
// open tcpsocket with connect
// wait for greeting
if(data.asInteger() == 220) {
out << "EHLO " << "mail.barant.com" << CRLF;
state = READY;
}
std::string SendMail::readLine(int fd) {
std::string line;
char ch;
while (::read(fd, &ch, 1) == 1) {
if (ch == '\n')
break;
if (ch != '\r')
line += ch;
}
return line;
}
case READY:
buffer = data;
setTimer(.05);
stateOnTimeout = READYX;
void SendMail::sendLine(int fd, const std::string &line, coreutils::File &log) {
std::string wire = line + "\r\n";
::write(fd, wire.c_str(), wire.length());
log.write(">> " + line + "\n");
}
int SendMail::readResponse(int fd, coreutils::File &log, std::string &fullText) {
// A multi-line SMTP reply repeats the same 3-digit code on every
// line, with "-" (not a space) after the code on every line but
// the last (RFC 5321 4.2.1) - keep reading until a line uses the
// space form.
int code = -1;
fullText.clear();
while (true) {
std::string line = readLine(fd);
log.write("<< " + line + "\n");
if (line.length() < 4)
return code;
code = atoi(line.substr(0, 3).c_str());
fullText += line + "\n";
if (line[3] == ' ')
break;
case READYX:
if(buffer.asInteger() == 250) {
out << "MAIL FROM:<" << from << ">" << CRLF;
state = MAIL;
}
return code;
}
bool SendMail::deliverToHost(const std::string &host, coreutils::File &log) {
struct addrinfo hints {};
struct addrinfo *result = nullptr;
// AF_UNSPEC let this pick a destination MX host's IPv6 address
// whenever one existed, which - since the OS picks the LOCAL
// source address to match the destination's address family - has
// this server originate the connection from its own IPv6 address
// instead of its IPv4 one. Confirmed live: every one of Gmail's 5
// MX hosts rejected delivery with "550 5.7.25 ... does not have a
// PTR record" for that IPv6 address, which really doesn't have
// one (nor is it covered by this domain's own SPF record, which
// only lists the IPv4 address - a second, independent reason
// IPv6 delivery from here can never pass real-world receiver
// policy). The IPv4 address DOES have a correct PTR (resolves
// back to barant.com) and IS the address barant.com's SPF record
// authorizes - AF_INET forces every outbound relay connection to
// originate from it, matching what this domain's actual DNS
// records promise. Revisit if IPv6 PTR + an ip6: SPF entry are
// ever added for this host.
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(host.c_str(), "25", &hints, &result) != 0 || !result) {
log.write("connect: could not resolve " + host + "\n");
return false;
}
int fd = -1;
for (struct addrinfo *rp = result; rp != nullptr; rp = rp->ai_next) {
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd < 0)
continue;
if (::connect(fd, rp->ai_addr, rp->ai_addrlen) == 0)
break;
case MAIL:
if(data.asInteger() == 250) {
out << "RCPT TO:<" << recipient << ">" << CRLF;
state = RCPT;
::close(fd);
fd = -1;
}
freeaddrinfo(result);
if (fd < 0) {
log.write("connect: unable to reach " + host + " on port 25\n");
return false;
}
struct timeval timeout {60, 0};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
std::string text;
// Everything but the final QUIT lives in this lambda so a failure
// partway through the conversation can bail out with a plain
// `return false` while the caller still always sends QUIT and
// closes the socket afterward, regardless of how this ended.
auto converse = [&]() -> bool {
if (readResponse(fd, log, text) / 100 != 2)
return false;
sendLine(fd, "EHLO " + hostName, log);
if (readResponse(fd, log, text) / 100 != 2) {
// Some older servers only speak HELO.
sendLine(fd, "HELO " + hostName, log);
if (readResponse(fd, log, text) / 100 != 2)
return false;
}
sendLine(fd, "MAIL FROM:<" + from.str() + ">", log);
if (readResponse(fd, log, text) / 100 != 2)
return false;
sendLine(fd, "RCPT TO:<" + recipient.str() + ">", log);
if (readResponse(fd, log, text) / 100 != 2)
return false;
sendLine(fd, "DATA", log);
if (readResponse(fd, log, text) != 354)
return false;
std::string body = mailData.str();
std::istringstream stream(body);
std::string bodyLine, wire;
while (std::getline(stream, bodyLine)) {
if (!bodyLine.empty() && bodyLine.back() == '\r')
bodyLine.pop_back();
// RFC 5321 4.5.2 transparency: a body line that legitimately
// starts with '.' must be stuffed with an extra leading '.'
// so the receiver never mistakes it for the terminating line.
if (!bodyLine.empty() && bodyLine[0] == '.')
wire += ".";
wire += bodyLine;
wire += "\r\n";
}
wire += ".\r\n";
::write(fd, wire.c_str(), wire.length());
log.write(">> [message body, " + std::to_string(body.length()) + " bytes]\n");
return readResponse(fd, log, text) / 100 == 2;
};
bool ok = converse();
sendLine(fd, "QUIT", log);
readResponse(fd, log, text);
::close(fd);
return ok;
}
bool SendMail::send() {
// ZString's std::string constructor takes a non-const reference,
// which can't bind to a temporary - a named local is required
// here rather than passing the concatenation expression directly.
std::string logFileName = sendLogPath + "/" + messageId + ".log";
coreutils::File log(logFileName, O_CREAT | O_WRONLY | O_APPEND, 0660);
log.write("=== delivery attempt: message " + messageId + " -> " + recipient.str() + " ===\n");
if (dkimSigner) {
// Signed once here, not inside deliverToHost() - deliverToHost()
// can run again against a second MX host if the first fails, and
// the signature (over this exact message content) is the same
// either way, so signing once upfront avoids redundant RSA ops
// and keeps every delivery attempt for this message consistent.
std::string signature = dkimSigner->sign(mailData.str());
coreutils::MString signedData;
signedData << signature.c_str() << mailData;
mailData = signedData;
log.write("DKIM-signed.\n");
}
std::string domain = domainOf(recipient);
std::vector<std::string> hosts = lookupMX(domain);
bool delivered = false;
for (auto &host : hosts) {
log.write("--- trying " + host + " ---\n");
if (deliverToHost(host, log)) {
log.write("delivered successfully via " + host + "\n");
delivered = true;
break;
case RCPT:
if(data.asInteger() == 250) {
out << "DATA" << CRLF;
state = DATA;
}
break;
case DATA:
if(data.asInteger() == 354) {
while(!mailData.eof()) {
mailData.readLine();
out << mailData.asZString() << CRLF;
}
out << "." << CRLF;
state = SENT;
}
break;
if (!delivered)
log.write("all hosts for " + domain + " failed\n");
case SENT:
if(data.asInteger() == 250) {
out << "QUIT" << CRLF;
}
break;
}
}
std::vector<coreutils::MString>& SendMail::extractRecipientList() {
return recipientList;
return delivered;
}
}
+56 -27
View File
@@ -1,45 +1,74 @@
#ifndef __SendMail_h__
#define __SendMail_h__
#include "EPoll.h"
#include "TCPSession2.h"
#include "DKIMSigner.h"
#include "File.h"
#include "MString.h"
#include "Timer.h"
#define CRLF "\r\n"
#define CRLF "\r\n"
#ifndef ___state_defined___
#define ___state_defined___
typedef enum {CONNECT, READY, READYX, MAIL, RCPT, DATA, SENT} State;
#endif
#include <string>
#include <vector>
namespace mail {
class SendMail : public core::TCPSession2, private core::Timer {
///
/// SendMail delivers one message to one remote recipient over
/// outbound SMTP (RFC 5321): resolves the recipient domain's MX
/// records - falling back to the domain's own address when none
/// exist, per RFC 5321 5.1 - tries each candidate host in preference
/// order until one accepts the message, and runs the client side of
/// the SMTP conversation (EHLO/HELO, MAIL FROM, RCPT TO, DATA with
/// dot-stuffing) over a plain blocking socket.
///
/// send() is a plain, synchronous, blocking call - it does not spawn
/// a thread or manage this object's lifetime itself, so the exact
/// same class serves both of this server's real callers even though
/// they want opposite things: SMTPServer::inCreate() (queued mail
/// found via inotify) wants fire-and-forget, so it wraps its own
/// heap-allocated SendMail in a detached thread and deletes it when
/// send() returns; main.cpp's "sendmail2" CLI-injector mode wants to
/// block and report the result back to its caller (matching the
/// classic local sendmail(1) contract other programs shell out to),
/// so it just calls send() directly on the stack. Baking a threading
/// policy into this class would have made one of those two wrong.
///
/// Every line sent and every line the remote server responds with is
/// appended, as it happens (not buffered to be written only at the
/// end - a hung or very slow remote host would otherwise leave no
/// trace at all until it finally times out), to one log file per
/// message under sendLogPath, shared across every recipient of that
/// message so its full delivery history reads as one transcript.
///
class SendMail {
public:
SendMail(core::EPoll &ePoll, std::string from, std::string recipient, std::string mailFileName);
SendMail(const SendMail &copy);
~SendMail();
coreutils::MString& send();
// dkimSigner is optional (nullptr = don't sign) and not owned -
// the caller keeps it alive for as long as any SendMail built
// from it might still be sending (a shared signer is fine to
// reuse across many messages; it holds no per-message state).
SendMail(std::string hostName, std::string sendLogPath, std::string messageId,
coreutils::MString from, coreutils::MString recipient, coreutils::MString mailData,
DKIMSigner *dkimSigner = nullptr);
void protocol(coreutils::ZString &data) override;
/// Blocking. Returns true if some MX host for the recipient's
/// domain accepted the message, false if DNS resolution or every
/// host it tried failed.
bool send();
private:
std::vector<coreutils::MString>& extractRecipientList();
void onTimeout() override;
bool deliverToHost(const std::string &host, coreutils::File &log);
std::vector<std::string> lookupMX(const std::string &domain);
std::string domainOf(coreutils::MString &address);
std::string readLine(int fd);
int readResponse(int fd, coreutils::File &log, std::string &fullText);
void sendLine(int fd, const std::string &line, coreutils::File &log);
core::EPoll &ePoll;
coreutils::File mailData;
std::string hostName;
std::string sendLogPath;
std::string messageId;
coreutils::MString from;
coreutils::MString recipient;
coreutils::MString mailFileName;
coreutils::MString buffer;
std::vector<coreutils::MString> recipientList;
State state = CONNECT;
State stateOnTimeout = CONNECT;
coreutils::MString mailData;
DKIMSigner *dkimSigner;
};
-184
View File
@@ -1,184 +0,0 @@
#include "SendMail2.h"
#include "ZString.h"
#include <chrono>
#include <string>
#include <cstdio>
#include <iostream>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/nameser.h>
#include <arpa/inet.h>
#include <resolv.h>
#include "Log.h"
#define BUFFER_SIZE 1048576
namespace mail {
SendMail2::SendMail2(core::EPoll &ePoll, coreutils::MString from, coreutils::MString recipient, coreutils::MString message)
: core::TCPSession2(ePoll, "Send Mail Injector"), from(from), recipient(recipient),
Timer(ePoll, 30.00f), ePoll(ePoll), message(message) {
coreutils::Log(coreutils::LOG_DEBUG_1) << "SendMail2 constructed.";
}
int SendMail2::send() {
waiter.lock();
coreutils::Log(coreutils::LOG_DEBUG_1) << this->recipient;
this->recipient.split("@");
coreutils::Log(coreutils::LOG_DEBUG_1) << this->recipient[1];
u_char nsbuff[4096];
char *domain = this->recipient[1].c_str();
int len = res_search(domain, C_IN, T_MX, nsbuff, sizeof(nsbuff));
ns_msg msg;
ns_initparse(nsbuff, len, &msg);
len = ns_msg_count(msg, ns_s_an);
char dispbuf[4096];
ns_rr rr;
for (int i = 0; i < 1; i++) {
ns_parserr(&msg, ns_s_an, i, &rr);
ns_sprintrr(&msg, &rr, NULL, NULL, dispbuf, sizeof(dispbuf));
}
coreutils::Log(coreutils::LOG_DEBUG_1) << "dispbuf: " << dispbuf;
coreutils::ZString mxArray(dispbuf);
mxArray.find("MX");
mxArray.ifNext("MX");
mxArray.skipWhitespace();
mxArray.getTokenInclude("0123456789");
mxArray.skipWhitespace();
coreutils::Log(coreutils::LOG_DEBUG_1) << "MX: " << mxArray.unparsed();
core::IPAddress mxAddress(mxArray.unparsed().c_str(), 25);
coreutils::Log(coreutils::LOG_DEBUG_1) << "IP: " << mxAddress.getClientAddressAndPort();
connect(mxAddress);
coreutils::Log(coreutils::LOG_DEBUG_1) << "SendMail2 injector initiated...";
return 0;
}
SendMail2::SendMail2(const SendMail2 &copy) : core::TCPSession2(ePoll, "Send Mail Injector"), from(copy.from), recipient(copy.recipient),
Timer(ePoll, 30.00f), ePoll(copy.ePoll), message(copy.message) {}
SendMail2::~SendMail2() {
coreutils::Log(coreutils::LOG_DEBUG_1) << "SendMail destructing...";
}
void SendMail2::onTimeout() {
coreutils::Log(coreutils::LOG_DEBUG_1) << "onTimeout..." << state;
returnValue << "timeout in " << state;
state = SENT;
waiter.unlock();
}
void SendMail2::protocol(coreutils::ZString &data) {
coreutils::Log(coreutils::LOG_DEBUG_1) << "[" << data << "..." << state;
switch (state) {
case CONNECT:
if(data.asInteger() == 220) {
out << "EHLO " << "drinkwise.barant.com" << CRLF;
coreutils::Log(coreutils::LOG_DEBUG_1) << ">EHLO";
state = READY;
}
break;
case READY:
buffer = data;
setTimer(30);
stateOnTimeout = READYX;
state = READYX;
break;
case READYX:
if(buffer.asInteger() == 250) {
coreutils::Log(coreutils::LOG_DEBUG_1) << ">MAIL FROM:<" << from << ">";
out << "MAIL FROM:<" << from << ">" << CRLF;
state = MAIL;
}
break;
case MAIL:
if(data.asInteger() == 250) {
coreutils::Log(coreutils::LOG_DEBUG_1) << ">RCPT TO:<" << recipient << ">";
out << "RCPT TO:<" << recipient << ">" << CRLF;
state = RCPT;
}
break;
case RCPT:
if(data.asInteger() == 250) {
out << "DATA" << CRLF;
state = DATA;
}
break;
case DATA:
if(data.asInteger() == 354) {
out << message << CRLF;
out << "." << CRLF;
state = SENT;
} else if (data.asInteger() == 421) {
coreutils::Log(coreutils::LOG_DEBUG_1) << "Error 421";
data.split(" ", 2);
returnValue = data;
state = SENT;
waiter.unlock();
} else if (data.asInteger() == 450) {
coreutils::Log(coreutils::LOG_DEBUG_1) << "Error 450";
data.split(" ", 2);
returnValue = data;
state = SENT;
waiter.unlock();
} else if (data.asInteger() == 451) {
coreutils::Log(coreutils::LOG_DEBUG_1) << "Error 451";
data.split(" ", 2);
returnValue = data;
state = SENT;
waiter.unlock();
} else if (data.asInteger() == 452) {
coreutils::Log(coreutils::LOG_DEBUG_1) << "Error 452";
data.split(" ", 2);
returnValue = data;
state = SENT;
waiter.unlock();
} else if (data.asInteger() == 550) {
data.split(" ", 2);
returnValue = data;
state = SENT;
waiter.unlock();
} else if (data.asInteger() == 552) {
coreutils::Log(coreutils::LOG_DEBUG_1) << "Error 552";
data.split(" ", 2);
returnValue = data;
state = SENT;
waiter.unlock();
}
break;
case SENT:
returnValue = data;
out << "QUIT" << CRLF;
waiter.unlock();
break;
}
}
coreutils::MString SendMail2::wait() {
waiter.lock();
waiter.unlock();
return returnValue;
}
std::vector<coreutils::MString>& SendMail2::extractRecipientList() {
return recipientList;
}
}
-51
View File
@@ -1,51 +0,0 @@
#ifndef __SendMail2_h__
#define __SendMail2_h__
#include "EPoll.h"
#include "TCPSession2.h"
#include "MString.h"
#include "Timer.h"
#include <mutex>
#define CRLF "\r\n"
#ifndef ___state_defined___
#define ___state_defined___
typedef enum {CONNECT, READY, READYX, MAIL, RCPT, DATA, SENT} State;
#endif
namespace mail {
class SendMail2 : public core::TCPSession2, private core::Timer {
public:
SendMail2(core::EPoll &ePoll, coreutils::MString from, coreutils::MString recipient, coreutils::MString message);
SendMail2(const SendMail2 &copy);
~SendMail2();
void protocol(coreutils::ZString &data) override;
int send();
coreutils::MString wait();
coreutils::MString returnValue;
private:
std::vector<coreutils::MString>& extractRecipientList();
void onTimeout() override;
core::EPoll &ePoll;
coreutils::MString from;
coreutils::MString recipient;
coreutils::MString buffer;
coreutils::MString message;
std::vector<coreutils::MString> recipientList;
State state = CONNECT;
State stateOnTimeout = CONNECT;
std::mutex waiter;
};
}
#endif
+112
View File
@@ -0,0 +1,112 @@
#include "SpamAssassinFilter.h"
#include "Log.h"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
namespace mail {
namespace {
const char *SPAMD_HOST = "127.0.0.1";
const int SPAMD_PORT = 783;
}
// Speaks the spamc/spamd wire protocol (documented in spamd(1)'s
// PROTOCOL file) directly over a plain blocking socket, rather than
// shelling out to the spamc binary - one connect/send/recv is
// simpler than managing a child process's stdin/stdout pipes
// without risking a full-pipe deadlock. This blocks whichever
// thread calls it for however long spamd takes to score the
// message - same tradeoff SPFChecker::check() already makes for its
// own synchronous DNS lookups (see __SMTP_MAIL.cpp's doc comment).
// Called only from SMTPServer::inCreate(), on the queue-watcher's
// EPoll worker thread, never inline in a client's DATA command - so
// it's this thread's other queued deliveries that wait behind a
// slow scan, not a connected client.
bool SpamAssassinFilter::talkToSpamd(const std::string &message, std::string &response) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
return false;
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(SPAMD_PORT);
if (inet_pton(AF_INET, SPAMD_HOST, &addr.sin_addr) != 1) {
close(fd);
return false;
}
if (connect(fd, (sockaddr *)&addr, sizeof(addr)) != 0) {
close(fd);
return false;
}
std::string request = "PROCESS SPAMC/1.5\r\n";
request += "Content-length: " + std::to_string(message.size()) + "\r\n";
request += "\r\n";
request += message;
size_t sent = 0;
while (sent < request.size()) {
ssize_t n = send(fd, request.data() + sent, request.size() - sent, 0);
if (n <= 0) {
close(fd);
return false;
}
sent += n;
}
shutdown(fd, SHUT_WR); // tell spamd it has the whole message, without closing our read side
response.clear();
char buf[8192];
ssize_t n;
while ((n = recv(fd, buf, sizeof(buf), 0)) > 0)
response.append(buf, n);
close(fd);
return !response.empty();
}
SpamAssassinFilter::Result SpamAssassinFilter::scan(const std::string &message) {
Result result;
result.scanned = message; // fail-open default: deliver the original if anything below bails out
std::string response;
if (!talkToSpamd(message, response)) {
coreutils::Log(coreutils::LOG_WARN) << "SpamAssassin scan failed (spamd unreachable at "
<< SPAMD_HOST << ":" << SPAMD_PORT << ") - delivering unscanned.";
return result;
}
// spamd's own response is itself a small header block
// ("SPAMD/1.5 0 EX_OK\r\nContent-length: N\r\n\r\n") wrapped
// around the scanned message - strip that off before treating
// what follows as the message to actually deliver.
size_t headerEnd = response.find("\r\n\r\n");
if (headerEnd == std::string::npos) {
coreutils::Log(coreutils::LOG_WARN) << "SpamAssassin scan returned an unparseable response - delivering unscanned.";
return result;
}
std::string statusLine = response.substr(0, response.find("\r\n"));
if (statusLine.find("EX_OK") == std::string::npos) {
coreutils::Log(coreutils::LOG_WARN) << "SpamAssassin scan reported an error (" << statusLine << ") - delivering unscanned.";
return result;
}
result.scanned = response.substr(headerEnd + 4);
result.ok = true;
// spamd's required_score (local.cf) is the actual threshold;
// this just reads the Yes/No verdict spamd already reached back
// out of the header it wrote, rather than re-deciding anything.
static const std::string marker = "\r\nX-Spam-Status: ";
size_t pos = result.scanned.find(marker);
if (pos != std::string::npos)
result.isSpam = result.scanned.compare(pos + marker.size(), 3, "Yes") == 0;
return result;
}
}
+47
View File
@@ -0,0 +1,47 @@
#ifndef __SpamAssassinFilter_h__
#define __SpamAssassinFilter_h__
#include <string>
namespace mail {
///
/// Pipes a message through the local spamd (via the spamc protocol,
/// spoken directly over a socket rather than shelling out to the
/// spamc binary) on the local-delivery path in
/// SMTPServer::inCreate() - spamd hands back the message with its
/// usual X-Spam-Status/X-Spam-Level/X-Spam-Report headers added (and,
/// once a message is over its own report_safe threshold, rewrapped
/// into a text/plain report). This class applies no threshold of its
/// own - spamd's required_score (/etc/spamassassin/local.cf, 5.0 on
/// this host) is what actually decides Yes/No; isSpam below just
/// reads that verdict back out of the X-Spam-Status header spamd
/// already wrote, the same way any other MTA's spam tag would be
/// read by a downstream filter.
///
/// Fails open: if spamd can't be reached at all, delivery proceeds
/// with the original, unscanned message rather than blocking mail
/// on a scanner outage.
///
class SpamAssassinFilter {
public:
struct Result {
bool ok = false; // true if spamd actually scanned the message
bool isSpam = false; // parsed from X-Spam-Status: Yes/No - meaningless if !ok
std::string scanned; // the (possibly tagged/rewritten) message; == input verbatim if !ok
};
static Result scan(const std::string &message);
private:
static bool talkToSpamd(const std::string &message, std::string &response);
};
}
#endif
+78 -2
View File
@@ -1,9 +1,85 @@
#include "__IMAP_APPEND.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
#include "MailFolder.h"
#include <cstdlib>
#include <sstream>
namespace mail {
int __IMAP_APPEND::processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
int __IMAP_APPEND::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
if (session.state == IMAPSession::NOT_AUTHENTICATED) {
session.out << session.tag << " NO Not authenticated" << CRLF;
return 1;
}
coreutils::ZString mailboxArg = IMAPArgs::nextArgument(request);
std::string folderName = IMAPArgs::normalizeFolderName(IMAPArgs::unwrap(mailboxArg));
coreutils::ZString userNameZ(session.userName);
if (!IMAPArgs::isValidFolderName(folderName)) {
session.out << session.tag << " BAD Invalid mailbox name" << CRLF;
return 1;
}
if (!server.mailFileSystem.ifFolderExists(userNameZ, folderName)) {
session.out << session.tag << " NO [TRYCREATE] Mailbox does not exist" << CRLF;
return 1;
}
std::vector<std::string> flags;
time_t internalDate = 0;
coreutils::ZString nextArg = IMAPArgs::nextArgument(request);
std::string nextText = nextArg.str();
if (!nextText.empty() && nextText[0] == '(') {
std::string flagText = IMAPArgs::unwrap(nextArg);
std::istringstream flagStream(flagText);
std::string flag;
while (flagStream >> flag)
flags.push_back(flag);
nextArg = IMAPArgs::nextArgument(request);
nextText = nextArg.str();
}
if (!nextText.empty() && nextText[0] == '"') {
internalDate = IMAPArgs::parseDate(nextArg);
if (internalDate == (time_t)-1)
internalDate = 0;
nextArg = IMAPArgs::nextArgument(request);
nextText = nextArg.str();
}
if (nextText.empty() || nextText[0] != '{') {
session.out << session.tag << " BAD APPEND requires a message literal" << CRLF;
return 1;
}
long literalSize = std::strtol(IMAPArgs::unwrap(nextArg).c_str(), nullptr, 10);
if (literalSize < 0) {
session.out << session.tag << " BAD Invalid literal size" << CRLF;
return 1;
}
std::string folderPath = server.mailFileSystem.getFolderPath(userNameZ, folderName).str();
std::string tag = session.tag;
bool wasSelectedFolder = (session.state == IMAPSession::SELECTED && session.selectedFolderName == folderName);
session.pendingBlockHandler = [&session, folderPath, flags, internalDate, tag, wasSelectedFolder](coreutils::ZString &block) {
session.setBlockSize(0);
MailFolder folder;
folder.load(folderPath);
uint32_t uid = folder.append(block.str(), flags, internalDate);
// A client that APPENDs into the mailbox it currently has
// SELECTed needs its own view refreshed, or a subsequent FETCH/
// STORE against the sequence numbers it already knows would be
// working off a stale EXISTS count.
if (wasSelectedFolder)
session.mailbox = folder;
session.out << tag << " OK [APPENDUID " << (int)folder.uidValidity() << " " << (int)uid << "] APPEND completed" << CRLF;
session.send();
};
session.out << "+ OK" << CRLF;
session.send();
session.setBlockSize((int)literalSize);
return 1;
}
+1 -1
View File
@@ -7,7 +7,7 @@ namespace mail {
class __IMAP_APPEND : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session);
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+5 -2
View File
@@ -7,8 +7,11 @@ namespace mail {
class __IMAP_AUTHENTICATE : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override {
// Still a placeholder: this command dispatches and replies with a
// syntactically valid IMAP tagged response now, but does not yet do
// any real mailbox work.
session.out << session.tag << " OK Completed" << CRLF;
return 1;
}
+23
View File
@@ -0,0 +1,23 @@
#include "__IMAP_CAPABILITY.h"
#include "IMAPServer.h"
namespace mail {
// RFC 3501 6.1.1: CAPABILITY MUST be supported in every connection
// state - it was previously not registered at all. Deliberately
// advertises nothing beyond bare IMAP4rev1: the greeting used to
// claim LITERAL+/SASL-IR/LOGIN-REFERRALS/ID/ENABLE/IDLE/STARTTLS/
// AUTH=PLAIN/AUTH=LOGIN, none of which had (or have) a working
// handler behind them - a client that believed any of those and
// tried to use it would get nothing back. Every other IMAP command
// this server registers still only returns a placeholder response
// (see each handler's own file) - claiming a capability whose
// handler doesn't yet do anything real would just move the same
// dishonesty here instead of fixing it.
int __IMAP_CAPABILITY::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
session.out << "* CAPABILITY IMAP4rev1" << CRLF;
session.out << session.tag << " OK CAPABILITY completed" << CRLF;
return 1;
}
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef ____IMAP_CAPABILITY_h__
# define ____IMAP_CAPABILITY_h__
# include "IMAPCommand.h"
namespace mail {
class __IMAP_CAPABILITY : public IMAPCommand {
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
}
#endif
+23
View File
@@ -0,0 +1,23 @@
#include "__IMAP_CHECK.h"
#include "IMAPServer.h"
namespace mail {
void refreshSelectedMailbox(IMAPSession &session) {
if (session.state != IMAPSession::SELECTED)
return;
size_t before = session.mailbox.size();
session.mailbox.load(session.mailbox.path);
if (session.mailbox.size() != before || session.mailbox.recentCount() > 0) {
session.out << "* " << (int)session.mailbox.size() << " EXISTS" << CRLF;
session.out << "* " << (int)session.mailbox.recentCount() << " RECENT" << CRLF;
}
}
int __IMAP_CHECK::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
refreshSelectedMailbox(session);
session.out << session.tag << " OK CHECK completed" << CRLF;
return 1;
}
}
+8 -4
View File
@@ -5,12 +5,16 @@
namespace mail {
// Re-loads the selected mailbox (if any) and emits fresh untagged
// EXISTS/RECENT if the message count changed - shared by CHECK and
// NOOP, RFC 3501 6.1.2/6.4.1 both call for exactly this ("perform
// housekeeping"/reflect newly delivered mail), and there's nothing
// NOOP does that CHECK doesn't already need to as its main job.
void refreshSelectedMailbox(IMAPSession &session);
class __IMAP_CHECK : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+25
View File
@@ -0,0 +1,25 @@
#include "__IMAP_CLOSE.h"
#include "IMAPServer.h"
namespace mail {
// RFC 3501 6.4.2: CLOSE expunges (silently - no untagged EXPUNGE)
// unless the mailbox was opened via EXAMINE, then always deselects.
int __IMAP_CLOSE::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
if (session.state != IMAPSession::SELECTED) {
session.out << session.tag << " NO No mailbox selected" << CRLF;
return 1;
}
if (!session.readOnly)
session.mailbox.expunge();
session.state = IMAPSession::AUTHENTICATED;
session.selectedFolderName.clear();
session.readOnly = false;
session.out << session.tag << " OK CLOSE completed" << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __IMAP_CLOSE : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+45
View File
@@ -0,0 +1,45 @@
#include "__IMAP_COPY.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
namespace mail {
int copyImpl(coreutils::ZString &request, IMAPSession &session, IMAPServer &server, bool uidMode) {
if (session.state != IMAPSession::SELECTED) {
session.out << session.tag << " NO No mailbox selected" << CRLF;
return 1;
}
std::vector<uint32_t> uids = IMAPArgs::parseSequenceSet(request, session.mailbox, uidMode);
coreutils::ZString nameArg = IMAPArgs::nextArgument(request);
std::string destName = IMAPArgs::normalizeFolderName(IMAPArgs::unwrap(nameArg));
coreutils::ZString userNameZ(session.userName);
if (!IMAPArgs::isValidFolderName(destName) || !server.mailFileSystem.ifFolderExists(userNameZ, destName)) {
session.out << session.tag << " NO [TRYCREATE] Destination mailbox does not exist" << CRLF;
return 1;
}
MailFolder destination;
destination.load(server.mailFileSystem.getFolderPath(userNameZ, destName).str());
for (uint32_t uid : uids) {
MailFolder::Message *message = session.mailbox.find(uid);
if (!message)
continue;
destination.append(session.mailbox.readMessage(uid), message->flags, message->internalDate);
}
if (session.state == IMAPSession::SELECTED && session.selectedFolderName == destName)
session.mailbox = destination;
session.out << session.tag << " OK " << (uidMode ? "UID COPY" : "COPY") << " completed" << CRLF;
return 1;
}
int __IMAP_COPY::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
return copyImpl(request, session, server, false);
}
}
+3 -4
View File
@@ -5,12 +5,11 @@
namespace mail {
int copyImpl(coreutils::ZString &request, IMAPSession &session, IMAPServer &server, bool uidMode);
class __IMAP_COPY : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+37
View File
@@ -0,0 +1,37 @@
#include "__IMAP_CREATE.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
#include <sys/stat.h>
namespace mail {
int __IMAP_CREATE::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
if (session.state == IMAPSession::NOT_AUTHENTICATED) {
session.out << session.tag << " NO Not authenticated" << CRLF;
return 1;
}
coreutils::ZString nameArg = IMAPArgs::nextArgument(request);
std::string folderName = IMAPArgs::normalizeFolderName(IMAPArgs::unwrap(nameArg));
coreutils::ZString userNameZ(session.userName);
if (!IMAPArgs::isValidFolderName(folderName)) {
session.out << session.tag << " BAD Invalid mailbox name" << CRLF;
return 1;
}
if (server.mailFileSystem.ifFolderExists(userNameZ, folderName)) {
session.out << session.tag << " NO Mailbox already exists" << CRLF;
return 1;
}
std::string path = server.mailFileSystem.getFolderPath(userNameZ, folderName).str();
if (mkdir(path.c_str(), 0770) != 0) {
session.out << session.tag << " NO CREATE failed" << CRLF;
return 1;
}
session.out << session.tag << " OK CREATE completed" << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __IMAP_CREATE : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+44
View File
@@ -0,0 +1,44 @@
#include "__IMAP_DELETE.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
#include <filesystem>
namespace mail {
int __IMAP_DELETE::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
if (session.state == IMAPSession::NOT_AUTHENTICATED) {
session.out << session.tag << " NO Not authenticated" << CRLF;
return 1;
}
coreutils::ZString nameArg = IMAPArgs::nextArgument(request);
std::string folderName = IMAPArgs::normalizeFolderName(IMAPArgs::unwrap(nameArg));
coreutils::ZString userNameZ(session.userName);
if (!IMAPArgs::isValidFolderName(folderName) || !server.mailFileSystem.ifFolderExists(userNameZ, folderName)) {
session.out << session.tag << " NO Mailbox does not exist" << CRLF;
return 1;
}
// RFC 3501 6.3.4 permits refusing to delete INBOX outright rather
// than implementing its special "move messages out, keep INBOX
// itself" semantics.
if (folderName == "Inbox") {
session.out << session.tag << " NO INBOX cannot be deleted" << CRLF;
return 1;
}
std::error_code errorCode;
std::filesystem::remove_all(server.mailFileSystem.getFolderPath(userNameZ, folderName).str(), errorCode);
if (errorCode) {
session.out << session.tag << " NO DELETE failed" << CRLF;
return 1;
}
if (session.state == IMAPSession::SELECTED && session.selectedFolderName == folderName)
session.state = IMAPSession::AUTHENTICATED;
session.out << session.tag << " OK DELETE completed" << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __IMAP_DELETE : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+10
View File
@@ -0,0 +1,10 @@
#include "__IMAP_EXAMINE.h"
#include "__IMAP_SELECT.h"
namespace mail {
int __IMAP_EXAMINE::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
return selectOrExamine(request, session, server, true);
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __IMAP_EXAMINE : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+27
View File
@@ -0,0 +1,27 @@
#include "__IMAP_EXPUNGE.h"
#include "IMAPServer.h"
namespace mail {
int __IMAP_EXPUNGE::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
if (session.state != IMAPSession::SELECTED) {
session.out << session.tag << " NO No mailbox selected" << CRLF;
return 1;
}
if (session.readOnly) {
session.out << session.tag << " NO Mailbox is read-only" << CRLF;
return 1;
}
// MailFolder::expunge() already returns each sequence number
// pre-adjusted for the ones reported before it - see its own doc
// comment for why that adjustment has to happen before, not
// after, these get sent.
for (uint32_t seqNum : session.mailbox.expunge())
session.out << "* " << (int)seqNum << " EXPUNGE" << CRLF;
session.out << session.tag << " OK EXPUNGE completed" << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __IMAP_EXPUNGE : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+302
View File
@@ -0,0 +1,302 @@
#include "__IMAP_FETCH.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
#include "MessageHeaders.h"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <ctime>
#include <sstream>
namespace mail {
namespace {
// Reads one FETCH data-item token, which - unlike an ordinary IMAP
// atom - may carry a "[section]" and/or "<start.length>" suffix
// with no space before them (BODY[HEADER.FIELDS (SUBJECT)]), and
// may itself contain embedded spaces INSIDE that bracketed
// section. IMAPArgs::nextArgument() can't handle this shape (it
// only recognizes a container when the very first character is
// one of "([{<" - "BODY[...]" starts with 'B').
std::string nextFetchItem(coreutils::ZString &items) {
items.skipWhitespace();
if (items.eod())
return "";
std::string token;
while (!items.eod() && items.getChar() != ' ' && items.getChar() != '[' && items.getChar() != '<')
token += items.nextChar();
if (!items.eod() && items.getChar() == '[') {
coreutils::ZString section = items.getContainer();
token += section.str();
}
if (!items.eod() && items.getChar() == '<') {
token += items.nextChar();
while (!items.eod() && items.getChar() != '>')
token += items.nextChar();
if (!items.eod())
token += items.nextChar();
}
return token;
}
std::string upper(const std::string &s) {
std::string result = s;
for (char &c : result)
c = std::toupper((unsigned char)c);
return result;
}
std::string formatInternalDate(time_t when) {
struct tm tmValue;
gmtime_r(&when, &tmValue);
char buf[64];
strftime(buf, sizeof(buf), "%d-%b-%Y %H:%M:%S +0000", &tmValue);
return buf;
}
std::string joinFlags(const std::vector<std::string> &flags) {
std::string out;
for (size_t i = 0; i < flags.size(); ++i) {
if (i > 0)
out += " ";
out += flags[i];
}
return out;
}
// Formats a section's content as an IMAP literal ({n}\r\n<bytes>),
// always - simpler and always correct, vs. quoted-string escaping
// for content that can contain arbitrary bytes/CRLFs.
std::string asLiteral(const std::string &content) {
std::ostringstream out;
out << "{" << content.size() << "}\r\n" << content;
return out.str();
}
// Extracts the section name out of a token like
// "BODY[HEADER.FIELDS (SUBJECT TO)]<0.100>" -> "HEADER.FIELDS (SUBJECT TO)",
// and separately reports any <start.length>/<start> range.
struct FetchItem {
std::string name; // e.g. "FLAGS", "BODY.PEEK", "RFC822.SIZE"
std::string section; // interior of [...], if any
bool hasSection = false;
bool hasRange = false;
long rangeStart = 0;
long rangeLength = -1; // -1 == to end
};
FetchItem parseFetchItem(const std::string &token) {
FetchItem item;
size_t bracket = token.find('[');
size_t angle = token.find('<');
size_t nameEnd = std::min(bracket, angle);
item.name = (nameEnd == std::string::npos) ? token : token.substr(0, nameEnd);
if (bracket != std::string::npos) {
size_t closeBracket = token.find(']', bracket);
if (closeBracket != std::string::npos) {
item.section = token.substr(bracket + 1, closeBracket - bracket - 1);
item.hasSection = true;
}
}
if (angle != std::string::npos) {
size_t closeAngle = token.find('>', angle);
if (closeAngle != std::string::npos) {
std::string rangeText = token.substr(angle + 1, closeAngle - angle - 1);
size_t dot = rangeText.find('.');
item.hasRange = true;
item.rangeStart = std::strtol(rangeText.substr(0, dot).c_str(), nullptr, 10);
if (dot != std::string::npos)
item.rangeLength = std::strtol(rangeText.substr(dot + 1).c_str(), nullptr, 10);
}
}
return item;
}
std::string applyRange(const std::string &content, const FetchItem &item) {
if (!item.hasRange)
return content;
if (item.rangeStart < 0 || (size_t)item.rangeStart >= content.size())
return "";
size_t length = item.rangeLength < 0 ? std::string::npos : (size_t)item.rangeLength;
return content.substr(item.rangeStart, length);
}
std::string sectionContent(const std::string &fullMessage, const std::string &sectionSpec) {
std::string headerBlock, bodyBlock;
splitMessage(fullMessage, headerBlock, bodyBlock);
std::string spec = upper(sectionSpec);
// Strip trailing whitespace introduced by our own tokenizing.
while (!spec.empty() && spec.back() == ' ')
spec.pop_back();
if (spec.empty())
return fullMessage;
if (spec == "TEXT")
return bodyBlock;
if (spec == "HEADER")
return headerBlock + "\r\n\r\n";
if (spec.rfind("HEADER.FIELDS", 0) == 0) {
size_t paren = sectionSpec.find('(');
size_t closeParen = sectionSpec.find(')', paren);
std::string fieldsText = (paren != std::string::npos && closeParen != std::string::npos)
? sectionSpec.substr(paren + 1, closeParen - paren - 1) : "";
std::istringstream fieldStream(fieldsText);
std::string fieldName;
auto headers = parseHeaderLines(headerBlock);
std::string result;
// Only fields actually present in the message are emitted -
// matching common server behavior for a requested field the
// message simply lacks.
while (fieldStream >> fieldName) {
for (auto &h : headers) {
if (h.first.size() != fieldName.size())
continue;
bool match = true;
for (size_t i = 0; i < fieldName.size(); ++i)
if (std::toupper((unsigned char)h.first[i]) != std::toupper((unsigned char)fieldName[i])) {
match = false;
break;
}
if (match)
result += h.first + ": " + h.second + "\r\n";
}
}
result += "\r\n";
return result;
}
// Unrecognized section shape (MIME part numbers, HEADER.FIELDS.NOT,
// etc. - out of scope, see plan's deferred-items list) - fall
// back to the whole message rather than fail outright.
return fullMessage;
}
}
int fetchImpl(coreutils::ZString &request, IMAPSession &session, IMAPServer &server, bool uidMode) {
if (session.state != IMAPSession::SELECTED) {
session.out << session.tag << " NO No mailbox selected" << CRLF;
return 1;
}
std::vector<uint32_t> uids = IMAPArgs::parseSequenceSet(request, session.mailbox, uidMode);
request.skipWhitespace();
std::vector<std::string> itemTokens;
if (!request.eod() && request.getChar() == '(') {
coreutils::ZString listArg = request.getContainer();
std::string interior = IMAPArgs::unwrap(listArg);
coreutils::ZString interiorZ(interior);
while (true) {
std::string item = nextFetchItem(interiorZ);
if (item.empty())
break;
itemTokens.push_back(item);
}
} else {
std::string item = nextFetchItem(request);
if (!item.empty())
itemTokens.push_back(item);
}
if (itemTokens.size() == 1) {
std::string macro = upper(itemTokens[0]);
if (macro == "ALL")
itemTokens = {"FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE"};
else if (macro == "FAST")
itemTokens = {"FLAGS", "INTERNALDATE", "RFC822.SIZE"};
else if (macro == "FULL")
itemTokens = {"FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE"};
}
for (uint32_t uid : uids) {
MailFolder::Message *message = session.mailbox.find(uid);
if (!message)
continue;
uint32_t seqNum = session.mailbox.seq(uid);
bool needsContent = false;
for (const std::string &token : itemTokens) {
std::string name = upper(parseFetchItem(token).name);
if (name == "ENVELOPE" || name == "RFC822" || name == "RFC822.HEADER" ||
name == "RFC822.TEXT" || name.rfind("BODY", 0) == 0)
needsContent = true;
}
std::string content = needsContent ? session.mailbox.readMessage(uid) : "";
bool markSeen = false;
std::ostringstream line;
bool first = true;
bool sawFlags = false;
auto emit = [&](const std::string &text) {
if (!first)
line << " ";
first = false;
line << text;
};
for (const std::string &token : itemTokens) {
FetchItem item = parseFetchItem(token);
std::string name = upper(item.name);
if (name == "FLAGS") {
emit("FLAGS (" + joinFlags(message->flags) + ")");
sawFlags = true;
} else if (name == "UID") {
emit("UID " + std::to_string(uid));
} else if (name == "INTERNALDATE") {
emit("INTERNALDATE " + imapQuoteOrNil(formatInternalDate(message->internalDate)));
} else if (name == "RFC822.SIZE") {
emit("RFC822.SIZE " + std::to_string(message->size));
} else if (name == "ENVELOPE") {
emit("ENVELOPE " + buildEnvelope(content));
} else if (name == "RFC822") {
emit("RFC822 " + asLiteral(applyRange(content, item)));
if (!session.readOnly) markSeen = true;
} else if (name == "RFC822.HEADER") {
std::string headerBlock, bodyBlock;
splitMessage(content, headerBlock, bodyBlock);
emit("RFC822.HEADER " + asLiteral(applyRange(headerBlock + "\r\n\r\n", item)));
} else if (name == "RFC822.TEXT") {
std::string headerBlock, bodyBlock;
splitMessage(content, headerBlock, bodyBlock);
emit("RFC822.TEXT " + asLiteral(applyRange(bodyBlock, item)));
if (!session.readOnly) markSeen = true;
} else if (name == "BODY" || name == "BODY.PEEK") {
std::string sectionText = item.hasSection ? sectionContent(content, item.section) : content;
std::string responseKey = "BODY[" + item.section + "]";
if (item.hasRange)
responseKey += "<" + std::to_string(item.rangeStart) + ">";
emit(responseKey + " " + asLiteral(applyRange(sectionText, item)));
bool isPeek = (name == "BODY.PEEK");
std::string upperSection = upper(item.section);
bool headerOnly = (upperSection == "HEADER" || upperSection.rfind("HEADER.FIELDS", 0) == 0);
if (!isPeek && !headerOnly && !session.readOnly)
markSeen = true;
} else {
// Unrecognized item (BODYSTRUCTURE, bare BODY macro, etc. -
// see the plan's explicitly-deferred list) - acknowledge
// with NIL rather than silently drop the field.
emit(upper(token) + " NIL");
}
}
if (markSeen && std::find(message->flags.begin(), message->flags.end(), "\\Seen") == message->flags.end()) {
message->flags.push_back("\\Seen");
session.mailbox.setFlags(uid, message->flags);
if (!sawFlags)
emit("FLAGS (" + joinFlags(message->flags) + ")");
}
session.out << "* " << (int)seqNum << " FETCH (" << line.str() << ")" << CRLF;
}
session.out << session.tag << " OK " << (uidMode ? "UID FETCH" : "FETCH") << " completed" << CRLF;
return 1;
}
int __IMAP_FETCH::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
return fetchImpl(request, session, server, false);
}
}
+8 -4
View File
@@ -5,12 +5,16 @@
namespace mail {
// Shared by FETCH and "UID FETCH" (see __IMAP_UID.cpp) - uidMode
// controls both how the sequence-set argument is interpreted and
// whether UID is force-included in every response line (RFC 3501
// 6.4.8: UID FETCH always returns UID whether or not it was asked
// for, so a client can tell which message is which).
int fetchImpl(coreutils::ZString &request, IMAPSession &session, IMAPServer &server, bool uidMode);
class __IMAP_FETCH : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+57
View File
@@ -0,0 +1,57 @@
#include "__IMAP_LIST.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
namespace mail {
bool imapListPatternMatch(const std::string &pattern, const std::string &name) {
size_t p = 0, n = 0, star = std::string::npos, matchPos = 0;
while (n < name.size()) {
if (p < pattern.size() && pattern[p] == name[n]) {
++p; ++n;
} else if (p < pattern.size() && (pattern[p] == '*' || pattern[p] == '%')) {
star = p;
matchPos = n;
++p;
} else if (star != std::string::npos) {
p = star + 1;
++matchPos;
n = matchPos;
} else {
return false;
}
}
while (p < pattern.size() && (pattern[p] == '*' || pattern[p] == '%'))
++p;
return p == pattern.size();
}
int __IMAP_LIST::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
if (session.state == IMAPSession::NOT_AUTHENTICATED) {
session.out << session.tag << " NO Not authenticated" << CRLF;
return 1;
}
IMAPArgs::nextArgument(request); // reference name - unused, no folder hierarchy to resolve against
coreutils::ZString patArg = IMAPArgs::nextArgument(request);
std::string pattern = IMAPArgs::unwrap(patArg);
if (pattern.empty()) {
// RFC 3501 6.3.8: an empty mailbox name means "return the
// hierarchy delimiter and the root name only".
session.out << "* LIST (\\Noselect) \"/\" \"\"" << CRLF;
session.out << session.tag << " OK LIST completed" << CRLF;
return 1;
}
coreutils::ZString userNameZ(session.userName);
for (coreutils::MString &folder : server.mailFileSystem.listFolders(userNameZ)) {
std::string name = folder.str();
if (imapListPatternMatch(pattern, name))
session.out << "* LIST (\\HasNoChildren) \"/\" \"" << name << "\"" << CRLF;
}
session.out << session.tag << " OK LIST completed" << CRLF;
return 1;
}
}
+9 -4
View File
@@ -2,15 +2,20 @@
# define ____IMAP_LIST_h__
# include "IMAPCommand.h"
# include <string>
namespace mail {
// Minimal glob match for LIST/LSUB patterns: '*' matches any run of
// characters, '%' does too (there's no folder hierarchy in this
// server, so the usual '%' "don't cross the hierarchy delimiter"
// distinction from '*' has nothing to differ over), anything else
// matches literally.
bool imapListPatternMatch(const std::string &pattern, const std::string &name);
class __IMAP_LIST : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+33
View File
@@ -0,0 +1,33 @@
#include "__IMAP_LOGIN.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
namespace mail {
int __IMAP_LOGIN::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
coreutils::ZString userArg = IMAPArgs::nextArgument(request);
coreutils::ZString passArg = IMAPArgs::nextArgument(request);
if (userArg.getLength() == 0 || passArg.getLength() == 0) {
session.out << session.tag << " BAD LOGIN requires a userid and password" << CRLF;
return 1;
}
std::string userName = IMAPArgs::unwrap(userArg);
std::string password = IMAPArgs::unwrap(passArg);
coreutils::ZString userNameZ(userName);
coreutils::ZString passwordZ(password);
// checkPassword()/getMailBoxPath() (MailFileSystem) expect a full
// "user@domain" address, the same convention SMTP AUTH LOGIN
// already uses for session.userName.
if (server.mailFileSystem.checkPassword(userNameZ, passwordZ)) {
session.userName = userName;
session.state = IMAPSession::AUTHENTICATED;
session.out << session.tag << " OK LOGIN completed" << CRLF;
} else {
session.out << session.tag << " NO LOGIN failed" << CRLF;
}
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __IMAP_LOGIN : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+20
View File
@@ -0,0 +1,20 @@
#include "__IMAP_LOGOUT.h"
#include "IMAPServer.h"
namespace mail {
// RFC 3501 6.1.3: LOGOUT MUST be supported in every connection
// state, replies with an untagged BYE then a tagged OK, and closes
// the connection - previously not registered at all, so a client
// had no clean way to end a session. terminate() (inherited from
// TCPSession) defers the actual close until after this response has
// gone out - TCPSession::onLineReceived() calls send() before
// checking the term flag.
int __IMAP_LOGOUT::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
session.out << "* BYE IMAP4rev1 Server logging out" << CRLF;
session.out << session.tag << " OK LOGOUT completed" << CRLF;
session.terminate();
return 1;
}
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef ____IMAP_LOGOUT_h__
# define ____IMAP_LOGOUT_h__
# include "IMAPCommand.h"
namespace mail {
class __IMAP_LOGOUT : public IMAPCommand {
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
}
#endif
+40
View File
@@ -0,0 +1,40 @@
#include "__IMAP_LSUB.h"
#include "__IMAP_LIST.h"
#include "__IMAP_SUBSCRIBE.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
namespace mail {
int __IMAP_LSUB::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
if (session.state == IMAPSession::NOT_AUTHENTICATED) {
session.out << session.tag << " NO Not authenticated" << CRLF;
return 1;
}
IMAPArgs::nextArgument(request); // reference name - unused
coreutils::ZString patArg = IMAPArgs::nextArgument(request);
std::string pattern = IMAPArgs::unwrap(patArg);
if (pattern.empty()) {
session.out << "* LSUB (\\Noselect) \"/\" \"\"" << CRLF;
session.out << session.tag << " OK LSUB completed" << CRLF;
return 1;
}
coreutils::ZString userNameZ(session.userName);
std::string mailboxPath = server.mailFileSystem.getMailBoxPath(userNameZ).str();
std::vector<std::string> allFolders;
for (coreutils::MString &folder : server.mailFileSystem.listFolders(userNameZ))
allFolders.push_back(folder.str());
std::vector<std::string> subscriptions = readSubscriptions(mailboxPath, allFolders);
for (const std::string &name : subscriptions)
if (imapListPatternMatch(pattern, name))
session.out << "* LSUB (\\HasNoChildren) \"/\" \"" << name << "\"" << CRLF;
session.out << session.tag << " OK LSUB completed" << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __IMAP_LSUB : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+26
View File
@@ -0,0 +1,26 @@
#ifndef ____IMAP_NOOP_h__
# define ____IMAP_NOOP_h__
# include "IMAPCommand.h"
# include "__IMAP_CHECK.h"
namespace mail {
class __IMAP_NOOP : public IMAPCommand {
// RFC 3501 6.1.2: NOOP MUST be supported in every connection
// state. Beyond replying OK, it's also the standard way an idle
// client polls for new mail without a full re-SELECT - shares
// refreshSelectedMailbox() with CHECK (see __IMAP_CHECK.h) so both
// report fresh EXISTS/RECENT when the mailbox has changed.
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override {
refreshSelectedMailbox(session);
session.out << session.tag << " OK NOOP completed" << CRLF;
return 1;
}
};
}
#endif
+52
View File
@@ -0,0 +1,52 @@
#include "__IMAP_RENAME.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
#include <filesystem>
namespace mail {
int __IMAP_RENAME::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
if (session.state == IMAPSession::NOT_AUTHENTICATED) {
session.out << session.tag << " NO Not authenticated" << CRLF;
return 1;
}
coreutils::ZString fromArg = IMAPArgs::nextArgument(request);
coreutils::ZString toArg = IMAPArgs::nextArgument(request);
std::string fromName = IMAPArgs::normalizeFolderName(IMAPArgs::unwrap(fromArg));
std::string toName = IMAPArgs::normalizeFolderName(IMAPArgs::unwrap(toArg));
coreutils::ZString userNameZ(session.userName);
if (!IMAPArgs::isValidFolderName(fromName) || !IMAPArgs::isValidFolderName(toName)) {
session.out << session.tag << " BAD Invalid mailbox name" << CRLF;
return 1;
}
if (fromName == "Inbox") {
session.out << session.tag << " NO INBOX cannot be renamed" << CRLF;
return 1;
}
if (!server.mailFileSystem.ifFolderExists(userNameZ, fromName)) {
session.out << session.tag << " NO Mailbox does not exist" << CRLF;
return 1;
}
if (server.mailFileSystem.ifFolderExists(userNameZ, toName)) {
session.out << session.tag << " NO Destination mailbox already exists" << CRLF;
return 1;
}
std::error_code errorCode;
std::filesystem::rename(server.mailFileSystem.getFolderPath(userNameZ, fromName).str(),
server.mailFileSystem.getFolderPath(userNameZ, toName).str(), errorCode);
if (errorCode) {
session.out << session.tag << " NO RENAME failed" << CRLF;
return 1;
}
if (session.state == IMAPSession::SELECTED && session.selectedFolderName == fromName)
session.selectedFolderName = toName;
session.out << session.tag << " OK RENAME completed" << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __IMAP_RENAME : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+214
View File
@@ -0,0 +1,214 @@
#include "__IMAP_SEARCH.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
#include "MessageHeaders.h"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <ctime>
#include <functional>
#include <memory>
namespace mail {
namespace {
using Predicate = std::function<bool(MailFolder &, const MailFolder::Message &)>;
bool hasFlag(const MailFolder::Message &m, const char *flag) {
return std::find(m.flags.begin(), m.flags.end(), flag) != m.flags.end();
}
std::string upperOf(const std::string &s) {
std::string r = s;
for (char &c : r)
c = std::toupper((unsigned char)c);
return r;
}
bool containsCI(const std::string &haystack, const std::string &needle) {
if (needle.empty())
return true;
return upperOf(haystack).find(upperOf(needle)) != std::string::npos;
}
time_t midnightUTC(time_t t) {
struct tm tmValue;
gmtime_r(&t, &tmValue);
tmValue.tm_hour = 0;
tmValue.tm_min = 0;
tmValue.tm_sec = 0;
return timegm(&tmValue);
}
Predicate headerContains(const std::string &headerName, const std::string &needle) {
return [headerName, needle](MailFolder &folder, const MailFolder::Message &m) {
std::string content = folder.readMessage(m.uid);
std::string headerBlock, bodyBlock;
splitMessage(content, headerBlock, bodyBlock);
auto headers = parseHeaderLines(headerBlock);
return containsCI(getHeader(headers, headerName), needle);
};
}
Predicate parseOneKey(coreutils::ZString &request, MailFolder &folder) {
coreutils::ZString keyArg = IMAPArgs::nextArgument(request);
std::string keyText = keyArg.str();
if (keyText.empty())
return [](MailFolder &, const MailFolder::Message &) { return true; };
if (std::isdigit((unsigned char)keyText[0]) || keyText[0] == '*') {
coreutils::ZString setOnly(keyText);
auto uids = std::make_shared<std::vector<uint32_t>>(IMAPArgs::parseSequenceSet(setOnly, folder, false));
return [uids](MailFolder &, const MailFolder::Message &m) {
return std::find(uids->begin(), uids->end(), m.uid) != uids->end();
};
}
std::string key = upperOf(keyText);
if (key == "ALL")
return [](MailFolder &, const MailFolder::Message &) { return true; };
if (key == "ANSWERED")
return [](MailFolder &, const MailFolder::Message &m) { return hasFlag(m, "\\Answered"); };
if (key == "UNANSWERED")
return [](MailFolder &, const MailFolder::Message &m) { return !hasFlag(m, "\\Answered"); };
if (key == "DELETED")
return [](MailFolder &, const MailFolder::Message &m) { return hasFlag(m, "\\Deleted"); };
if (key == "UNDELETED")
return [](MailFolder &, const MailFolder::Message &m) { return !hasFlag(m, "\\Deleted"); };
if (key == "SEEN")
return [](MailFolder &, const MailFolder::Message &m) { return hasFlag(m, "\\Seen"); };
if (key == "UNSEEN")
return [](MailFolder &, const MailFolder::Message &m) { return !hasFlag(m, "\\Seen"); };
if (key == "FLAGGED")
return [](MailFolder &, const MailFolder::Message &m) { return hasFlag(m, "\\Flagged"); };
if (key == "UNFLAGGED")
return [](MailFolder &, const MailFolder::Message &m) { return !hasFlag(m, "\\Flagged"); };
if (key == "DRAFT")
return [](MailFolder &, const MailFolder::Message &m) { return hasFlag(m, "\\Draft"); };
if (key == "UNDRAFT")
return [](MailFolder &, const MailFolder::Message &m) { return !hasFlag(m, "\\Draft"); };
if (key == "NEW")
return [](MailFolder &, const MailFolder::Message &m) { return m.recent && !hasFlag(m, "\\Seen"); };
if (key == "OLD")
return [](MailFolder &, const MailFolder::Message &m) { return !m.recent; };
if (key == "RECENT")
return [](MailFolder &, const MailFolder::Message &m) { return m.recent; };
if (key == "UID") {
coreutils::ZString setArg = IMAPArgs::nextArgument(request);
std::string setText = setArg.str();
coreutils::ZString setOnly(setText);
auto uids = std::make_shared<std::vector<uint32_t>>(IMAPArgs::parseSequenceSet(setOnly, folder, true));
return [uids](MailFolder &, const MailFolder::Message &m) {
return std::find(uids->begin(), uids->end(), m.uid) != uids->end();
};
}
if (key == "FROM" || key == "TO" || key == "CC" || key == "BCC" || key == "SUBJECT") {
coreutils::ZString valueArg = IMAPArgs::nextArgument(request);
std::string needle = IMAPArgs::unwrap(valueArg);
std::string headerName = key == "FROM" ? "From" : key == "TO" ? "To" :
key == "CC" ? "Cc" : key == "BCC" ? "Bcc" : "Subject";
return headerContains(headerName, needle);
}
if (key == "BODY") {
coreutils::ZString valueArg = IMAPArgs::nextArgument(request);
std::string needle = IMAPArgs::unwrap(valueArg);
return [needle](MailFolder &folder, const MailFolder::Message &m) {
std::string content = folder.readMessage(m.uid);
std::string headerBlock, bodyBlock;
splitMessage(content, headerBlock, bodyBlock);
return containsCI(bodyBlock, needle);
};
}
if (key == "TEXT") {
coreutils::ZString valueArg = IMAPArgs::nextArgument(request);
std::string needle = IMAPArgs::unwrap(valueArg);
return [needle](MailFolder &folder, const MailFolder::Message &m) {
return containsCI(folder.readMessage(m.uid), needle);
};
}
if (key == "BEFORE" || key == "ON" || key == "SINCE") {
coreutils::ZString dateArg = IMAPArgs::nextArgument(request);
time_t target = IMAPArgs::parseDate(dateArg);
if (target == (time_t)-1)
return [](MailFolder &, const MailFolder::Message &) { return true; };
target = midnightUTC(target);
if (key == "BEFORE")
return [target](MailFolder &, const MailFolder::Message &m) { return midnightUTC(m.internalDate) < target; };
if (key == "SINCE")
return [target](MailFolder &, const MailFolder::Message &m) { return midnightUTC(m.internalDate) >= target; };
return [target](MailFolder &, const MailFolder::Message &m) { return midnightUTC(m.internalDate) == target; };
}
if (key == "LARGER" || key == "SMALLER") {
coreutils::ZString numArg = IMAPArgs::nextArgument(request);
long value = std::strtol(numArg.str().c_str(), nullptr, 10);
if (key == "LARGER")
return [value](MailFolder &, const MailFolder::Message &m) { return (long)m.size > value; };
return [value](MailFolder &, const MailFolder::Message &m) { return (long)m.size < value; };
}
if (key == "NOT") {
Predicate inner = parseOneKey(request, folder);
return [inner](MailFolder &folder, const MailFolder::Message &m) { return !inner(folder, m); };
}
if (key == "OR") {
Predicate left = parseOneKey(request, folder);
Predicate right = parseOneKey(request, folder);
return [left, right](MailFolder &folder, const MailFolder::Message &m) {
return left(folder, m) || right(folder, m);
};
}
// Unrecognized keyword (CHARSET, HEADER, KEYWORD, SENT*,
// SEQUENCE numbers already handled above - see the plan's
// deferred-items list) - treated as always-true rather than
// aborting the whole SEARCH.
return [](MailFolder &, const MailFolder::Message &) { return true; };
}
}
int searchImpl(coreutils::ZString &request, IMAPSession &session, IMAPServer &server, bool uidMode) {
if (session.state != IMAPSession::SELECTED) {
session.out << session.tag << " NO No mailbox selected" << CRLF;
return 1;
}
std::vector<Predicate> predicates;
while (true) {
request.skipWhitespace();
if (request.eod())
break;
predicates.push_back(parseOneKey(request, session.mailbox));
}
session.out << "* SEARCH";
for (const MailFolder::Message &message : session.mailbox.messages()) {
bool matched = true;
for (const Predicate &predicate : predicates) {
if (!predicate(session.mailbox, message)) {
matched = false;
break;
}
}
if (matched)
session.out << " " << (int)(uidMode ? message.uid : session.mailbox.seq(message.uid));
}
session.out << CRLF;
session.out << session.tag << " OK " << (uidMode ? "UID SEARCH" : "SEARCH") << " completed" << CRLF;
return 1;
}
int __IMAP_SEARCH::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
return searchImpl(request, session, server, false);
}
}
+3 -4
View File
@@ -5,12 +5,11 @@
namespace mail {
int searchImpl(coreutils::ZString &request, IMAPSession &session, IMAPServer &server, bool uidMode);
class __IMAP_SEARCH : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+52
View File
@@ -0,0 +1,52 @@
#include "__IMAP_SELECT.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
namespace mail {
int selectOrExamine(coreutils::ZString &request, IMAPSession &session, IMAPServer &server, bool readOnly) {
if (session.state == IMAPSession::NOT_AUTHENTICATED) {
session.out << session.tag << " NO Not authenticated" << CRLF;
return 1;
}
coreutils::ZString nameArg = IMAPArgs::nextArgument(request);
std::string folderName = IMAPArgs::normalizeFolderName(IMAPArgs::unwrap(nameArg));
coreutils::ZString userNameZ(session.userName);
if (!IMAPArgs::isValidFolderName(folderName)) {
session.out << session.tag << " NO Mailbox does not exist" << CRLF;
return 1;
}
if (!server.mailFileSystem.ifFolderExists(userNameZ, folderName)) {
session.out << session.tag << " NO Mailbox does not exist" << CRLF;
return 1;
}
MailFolder folder;
folder.load(server.mailFileSystem.getFolderPath(userNameZ, folderName).str());
session.mailbox = folder;
session.selectedFolderName = folderName;
session.readOnly = readOnly;
session.state = IMAPSession::SELECTED;
session.out << "* " << (int)session.mailbox.size() << " EXISTS" << CRLF;
session.out << "* " << (int)session.mailbox.recentCount() << " RECENT" << CRLF;
uint32_t firstUnseen = session.mailbox.firstUnseenSeq();
if (firstUnseen != 0)
session.out << "* OK [UNSEEN " << (int)firstUnseen << "] Message " << (int)firstUnseen << " is first unseen" << CRLF;
session.out << "* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)" << CRLF;
session.out << "* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)] Limited" << CRLF;
session.out << "* OK [UIDVALIDITY " << (int)session.mailbox.uidValidity() << "] UIDs valid" << CRLF;
session.out << "* OK [UIDNEXT " << (int)session.mailbox.uidNext() << "] Predicted next UID" << CRLF;
session.out << session.tag << " OK [" << (readOnly ? "READ-ONLY" : "READ-WRITE") << "] "
<< (readOnly ? "EXAMINE" : "SELECT") << " completed" << CRLF;
return 1;
}
int __IMAP_SELECT::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
return selectOrExamine(request, session, server, false);
}
}
+8 -4
View File
@@ -5,12 +5,16 @@
namespace mail {
// Shared by SELECT and EXAMINE (RFC 3501 6.3.1/6.3.2 - identical
// except EXAMINE opens read-only and doesn't leave the mailbox
// vulnerable to STORE/EXPUNGE). Also used directly by __IMAP_UID and
// __IMAP_STATUS-adjacent code that needs a freshly loaded MailFolder
// without going through the full command dispatch.
int selectOrExamine(coreutils::ZString &request, IMAPSession &session, IMAPServer &server, bool readOnly);
class __IMAP_SELECT : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+5 -2
View File
@@ -7,8 +7,11 @@ namespace mail {
class __IMAP_STARTTLS : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override {
// Still a placeholder: this command dispatches and replies with a
// syntactically valid IMAP tagged response now, but does not yet do
// any real mailbox work.
session.out << session.tag << " OK Completed" << CRLF;
return 1;
}
+59
View File
@@ -0,0 +1,59 @@
#include "__IMAP_STATUS.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
#include "MailFolder.h"
#include <sstream>
namespace mail {
int __IMAP_STATUS::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
if (session.state == IMAPSession::NOT_AUTHENTICATED) {
session.out << session.tag << " NO Not authenticated" << CRLF;
return 1;
}
coreutils::ZString nameArg = IMAPArgs::nextArgument(request);
coreutils::ZString itemsArg = IMAPArgs::nextArgument(request);
std::string folderName = IMAPArgs::normalizeFolderName(IMAPArgs::unwrap(nameArg));
std::string itemsText = IMAPArgs::unwrap(itemsArg);
coreutils::ZString userNameZ(session.userName);
if (!IMAPArgs::isValidFolderName(folderName) || !server.mailFileSystem.ifFolderExists(userNameZ, folderName)) {
session.out << session.tag << " NO Mailbox does not exist" << CRLF;
return 1;
}
MailFolder folder;
folder.load(server.mailFileSystem.getFolderPath(userNameZ, folderName).str());
std::istringstream itemStream(itemsText);
std::string item;
std::ostringstream reply;
bool first = true;
while (itemStream >> item) {
if (!first)
reply << " ";
first = false;
std::string upper = item;
for (char &c : upper)
c = std::toupper((unsigned char)c);
if (upper == "MESSAGES")
reply << "MESSAGES " << (int)folder.size();
else if (upper == "RECENT")
reply << "RECENT " << (int)folder.recentCount();
else if (upper == "UIDNEXT")
reply << "UIDNEXT " << (int)folder.uidNext();
else if (upper == "UIDVALIDITY")
reply << "UIDVALIDITY " << (int)folder.uidValidity();
else if (upper == "UNSEEN")
reply << "UNSEEN " << (int)folder.unseenCount();
else
first = true; // unrecognized item - skip without leaving a dangling separator
}
session.out << "* STATUS \"" << folderName << "\" (" << reply.str() << ")" << CRLF;
session.out << session.tag << " OK STATUS completed" << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __IMAP_STATUS : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+80
View File
@@ -0,0 +1,80 @@
#include "__IMAP_STORE.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
#include <algorithm>
#include <cctype>
namespace mail {
namespace {
std::string joinFlags(const std::vector<std::string> &flags) {
std::string out;
for (size_t i = 0; i < flags.size(); ++i) {
if (i > 0)
out += " ";
out += flags[i];
}
return out;
}
}
int storeImpl(coreutils::ZString &request, IMAPSession &session, IMAPServer &server, bool uidMode) {
if (session.state != IMAPSession::SELECTED) {
session.out << session.tag << " NO No mailbox selected" << CRLF;
return 1;
}
if (session.readOnly) {
session.out << session.tag << " NO Mailbox is read-only" << CRLF;
return 1;
}
std::vector<uint32_t> uids = IMAPArgs::parseSequenceSet(request, session.mailbox, uidMode);
coreutils::ZString modeArg = IMAPArgs::nextArgument(request);
std::string mode = modeArg.str();
bool silent = false;
// Strip a trailing ".SILENT" (case-insensitive) before inspecting
// the leading +/- sign.
if (mode.size() >= 7) {
std::string tail = mode.substr(mode.size() - 7);
for (char &c : tail)
c = std::toupper((unsigned char)c);
if (tail == ".SILENT") {
silent = true;
mode = mode.substr(0, mode.size() - 7);
}
}
char sign = mode.empty() ? '=' : mode[0];
std::vector<std::string> flags = IMAPArgs::parseFlagList(request);
for (uint32_t uid : uids) {
MailFolder::Message *message = session.mailbox.find(uid);
if (!message)
continue;
if (sign == '+')
session.mailbox.addFlags(uid, flags);
else if (sign == '-')
session.mailbox.removeFlags(uid, flags);
else
session.mailbox.setFlags(uid, flags);
if (!silent) {
uint32_t seqNum = session.mailbox.seq(uid);
session.out << "* " << (int)seqNum << " FETCH (FLAGS (" << joinFlags(message->flags) << ")";
if (uidMode)
session.out << " UID " << (int)uid;
session.out << ")" << CRLF;
}
}
session.out << session.tag << " OK " << (uidMode ? "UID STORE" : "STORE") << " completed" << CRLF;
return 1;
}
int __IMAP_STORE::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
return storeImpl(request, session, server, false);
}
}
+3 -4
View File
@@ -5,12 +5,11 @@
namespace mail {
int storeImpl(coreutils::ZString &request, IMAPSession &session, IMAPServer &server, bool uidMode);
class __IMAP_STORE : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+56
View File
@@ -0,0 +1,56 @@
#include "__IMAP_SUBSCRIBE.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
#include <algorithm>
#include <fstream>
namespace mail {
std::vector<std::string> readSubscriptions(const std::string &mailboxPath, const std::vector<std::string> &allFolders) {
std::ifstream in(mailboxPath + "/.subscriptions");
if (!in)
return allFolders;
std::vector<std::string> result;
std::string line;
while (std::getline(in, line))
if (!line.empty())
result.push_back(line);
return result;
}
void writeSubscriptions(const std::string &mailboxPath, const std::vector<std::string> &subscriptions) {
std::ofstream out(mailboxPath + "/.subscriptions", std::ios::trunc);
for (const std::string &name : subscriptions)
out << name << "\n";
}
int __IMAP_SUBSCRIBE::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
if (session.state == IMAPSession::NOT_AUTHENTICATED) {
session.out << session.tag << " NO Not authenticated" << CRLF;
return 1;
}
coreutils::ZString nameArg = IMAPArgs::nextArgument(request);
std::string folderName = IMAPArgs::normalizeFolderName(IMAPArgs::unwrap(nameArg));
coreutils::ZString userNameZ(session.userName);
if (!IMAPArgs::isValidFolderName(folderName) || !server.mailFileSystem.ifFolderExists(userNameZ, folderName)) {
session.out << session.tag << " NO Mailbox does not exist" << CRLF;
return 1;
}
std::string mailboxPath = server.mailFileSystem.getMailBoxPath(userNameZ).str();
std::vector<std::string> allFolders;
for (coreutils::MString &folder : server.mailFileSystem.listFolders(userNameZ))
allFolders.push_back(folder.str());
std::vector<std::string> subscriptions = readSubscriptions(mailboxPath, allFolders);
if (std::find(subscriptions.begin(), subscriptions.end(), folderName) == subscriptions.end())
subscriptions.push_back(folderName);
writeSubscriptions(mailboxPath, subscriptions);
session.out << session.tag << " OK SUBSCRIBE completed" << CRLF;
return 1;
}
}
+12 -4
View File
@@ -2,15 +2,23 @@
# define ____IMAP_SUBSCRIBE_h__
# include "IMAPCommand.h"
# include <string>
# include <vector>
namespace mail {
// Backed by a plain one-name-per-line ".subscriptions" file at the
// mailbox root, alongside Inbox/Spam/Trash and .password. If that
// file doesn't exist yet (nothing has ever called SUBSCRIBE/
// UNSUBSCRIBE on this mailbox), every existing folder is treated as
// subscribed by default - matches how a freshly provisioned mailbox
// should behave for a client that only ever calls LSUB.
std::vector<std::string> readSubscriptions(const std::string &mailboxPath, const std::vector<std::string> &allFolders);
void writeSubscriptions(const std::string &mailboxPath, const std::vector<std::string> &subscriptions);
class __IMAP_SUBSCRIBE : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+31
View File
@@ -0,0 +1,31 @@
#include "__IMAP_UID.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
#include "__IMAP_FETCH.h"
#include "__IMAP_STORE.h"
#include "__IMAP_SEARCH.h"
#include "__IMAP_COPY.h"
#include <cctype>
namespace mail {
int __IMAP_UID::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
coreutils::ZString verbArg = IMAPArgs::nextArgument(request);
std::string verb = verbArg.str();
for (char &c : verb)
c = std::toupper((unsigned char)c);
if (verb == "FETCH")
return fetchImpl(request, session, server, true);
if (verb == "STORE")
return storeImpl(request, session, server, true);
if (verb == "SEARCH")
return searchImpl(request, session, server, true);
if (verb == "COPY")
return copyImpl(request, session, server, true);
session.out << session.tag << " BAD Unsupported UID subcommand" << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __IMAP_UID : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+32
View File
@@ -0,0 +1,32 @@
#include "__IMAP_UNSUBSCRIBE.h"
#include "__IMAP_SUBSCRIBE.h"
#include "IMAPArgs.h"
#include "IMAPServer.h"
#include <algorithm>
namespace mail {
int __IMAP_UNSUBSCRIBE::processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) {
if (session.state == IMAPSession::NOT_AUTHENTICATED) {
session.out << session.tag << " NO Not authenticated" << CRLF;
return 1;
}
coreutils::ZString nameArg = IMAPArgs::nextArgument(request);
std::string folderName = IMAPArgs::normalizeFolderName(IMAPArgs::unwrap(nameArg));
coreutils::ZString userNameZ(session.userName);
std::string mailboxPath = server.mailFileSystem.getMailBoxPath(userNameZ).str();
std::vector<std::string> allFolders;
for (coreutils::MString &folder : server.mailFileSystem.listFolders(userNameZ))
allFolders.push_back(folder.str());
std::vector<std::string> subscriptions = readSubscriptions(mailboxPath, allFolders);
subscriptions.erase(std::remove(subscriptions.begin(), subscriptions.end(), folderName), subscriptions.end());
writeSubscriptions(mailboxPath, subscriptions);
session.out << session.tag << " OK UNSUBSCRIBE completed" << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __IMAP_UNSUBSCRIBE : public IMAPCommand {
int processCommand(coreutils::ZString request, IMAPSession &session) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString &request, IMAPSession &session, IMAPServer &server) override;
};
+32
View File
@@ -0,0 +1,32 @@
#ifndef ____POP3_CAPA_h__
# define ____POP3_CAPA_h__
# include "POP3Command.h"
namespace mail {
// RFC 2449: CAPA is the standard way a POP3 client discovers what a
// server actually supports before trying it - this server had no way
// to advertise anything at all before. USER (the only login
// mechanism __POP3_USER.h/__POP3_PASS.h implement), PIPELINING
// (already true of every protocol built on this framework -
// TCPSocket::onDataReceived() loops over every complete line already
// buffered, not just the first one read), and now UIDL (real as of
// __POP3_UIDL.cpp). Still nothing claiming APOP, STLS, or SASL,
// since none of those have a real handler behind them.
class __POP3_CAPA : public POP3Command {
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) override {
session.out << "+OK Capability list follows" << CRLF;
session.out << "USER" << CRLF;
session.out << "UIDL" << CRLF;
session.out << "PIPELINING" << CRLF;
session.out << "." << CRLF;
return 1;
}
};
}
#endif
+25
View File
@@ -0,0 +1,25 @@
#include "__POP3_DELE.h"
#include "POP3Server.h"
namespace mail {
// RFC 1939 3: DELE only marks a message deleted for this session -
// the mark takes effect (actual unlink) only at QUIT, and RSET below
// can undo it beforehand.
int __POP3_DELE::processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) {
if (!session.authenticated) {
session.out << "-ERR Not authenticated" << CRLF;
return 1;
}
request.skipWhitespace();
int msgNum = request.getTokenExclude(" ").asInteger();
if (!pop3FindMessage(session, msgNum)) {
session.out << "-ERR No such message" << CRLF;
return 1;
}
session.deletedMarks.insert(msgNum);
session.out << "+OK Message " << msgNum << " deleted" << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __POP3_DELE : public POP3Command {
int processCommand(coreutils::ZString &request, POP3Session &session, POP3Server &server) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) override;
};
+6 -2
View File
@@ -7,8 +7,12 @@ namespace mail {
class __POP3_LAST : public POP3Command {
int processCommand(coreutils::ZString &request, POP3Session &session, POP3Server &server) {
session.out << "250 OK" << CRLF;
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) override {
// Still a placeholder: this command dispatches and replies with a
// syntactically valid POP3 response now ("+OK", not the SMTP-style
// "250 OK" this used to send), but does not yet do any real
// mailbox work.
session.out << "+OK" << CRLF;
return 1;
}
+43
View File
@@ -0,0 +1,43 @@
#include "__POP3_LIST.h"
#include "POP3Server.h"
namespace mail {
int __POP3_LIST::processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) {
if (!session.authenticated) {
session.out << "-ERR Not authenticated" << CRLF;
return 1;
}
request.skipWhitespace();
coreutils::ZString numArg = request.getTokenExclude(" ");
if (numArg.getLength() > 0) {
int msgNum = numArg.asInteger();
MailFolder::Message *message = pop3FindMessage(session, msgNum);
if (!message) {
session.out << "-ERR No such message" << CRLF;
return 1;
}
session.out << "+OK " << msgNum << " " << (int)message->size << CRLF;
return 1;
}
const auto &messages = session.mailbox.messages();
int count = 0;
long totalSize = 0;
for (size_t i = 0; i < messages.size(); ++i) {
if (session.deletedMarks.count((int)(i + 1)) == 0) {
++count;
totalSize += (long)messages[i].size;
}
}
session.out << "+OK " << count << " messages (" << totalSize << " octets)" << CRLF;
for (size_t i = 0; i < messages.size(); ++i)
if (session.deletedMarks.count((int)(i + 1)) == 0)
session.out << (i + 1) << " " << (int)messages[i].size << CRLF;
session.out << "." << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __POP3_LIST : public POP3Command {
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) override;
};
+5 -1
View File
@@ -8,7 +8,11 @@ namespace mail {
class __POP3_NOOP : public POP3Command {
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) {
session.out << "250 OK" << CRLF;
// Still a placeholder: this command dispatches and replies with a
// syntactically valid POP3 response now ("+OK", not the SMTP-style
// "250 OK" this used to send), but does not yet do any real
// mailbox work.
session.out << "+OK" << CRLF;
return 1;
}
+41
View File
@@ -0,0 +1,41 @@
#include "__POP3_PASS.h"
#include "POP3Server.h"
namespace mail {
int __POP3_PASS::processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) {
request.skipWhitespace();
// Deliberately the rest of the line, not just the first
// whitespace-delimited token - RFC 1939 4's PASS <string> may
// contain any character except CR/LF, spaces included. (A known,
// documented gap of this same shape exists nowhere else in this
// codebase for POP3, since every other POP3 argument is a bare
// number or single atom that's never expected to contain a
// space.)
coreutils::ZString passArg = request.unparsed();
if (session.userName.empty()) {
session.out << "-ERR USER required first" << CRLF;
return 1;
}
if (passArg.getLength() == 0) {
session.out << "-ERR Usage: PASS <password>" << CRLF;
return 1;
}
coreutils::ZString userNameZ(session.userName);
if (!server.mailFileSystem.checkPassword(userNameZ, passArg)) {
session.out << "-ERR Authentication failed" << CRLF;
session.userName.clear();
return 1;
}
coreutils::ZString userNameZ2(session.userName);
std::string mailboxPath = server.mailFileSystem.getFolderPath(userNameZ2, "Inbox").str();
session.mailbox.load(mailboxPath);
session.authenticated = true;
session.out << "+OK Logged in" << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __POP3_PASS : public POP3Command {
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) override;
};
+24 -2
View File
@@ -7,8 +7,30 @@ namespace mail {
class __POP3_QUIT : public POP3Command {
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) {
session.out << "250 OK" << CRLF;
// RFC 1939 6: the UPDATE state - QUIT is what actually commits
// every DELE mark made this session (unlinking those messages),
// not DELE itself. Only happens if the session ever authenticated
// (a QUIT sent before USER/PASS in the AUTHORIZATION state has no
// maildrop to touch).
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) override {
if (session.authenticated) {
// Resolve every marked message number to its UID FIRST, all
// at once, before deleting any of them - deleteMessage()
// mutates the mailbox's message list, which would shift
// every LATER message number's meaning if resolved one at a
// time interleaved with deletion. UIDs, unlike message
// numbers, are stable across that mutation.
std::vector<uint32_t> uidsToDelete;
for (int msgNum : session.deletedMarks) {
uint32_t uid = session.mailbox.uidForSeq(msgNum);
if (uid != 0)
uidsToDelete.push_back(uid);
}
for (uint32_t uid : uidsToDelete)
session.mailbox.deleteMessage(uid);
}
session.out << "+OK BARANTMail POP3 server signing off" << CRLF;
session.terminate();
return 1;
}
+23
View File
@@ -0,0 +1,23 @@
#include "__POP3_RETR.h"
#include "POP3Server.h"
namespace mail {
int __POP3_RETR::processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) {
if (!session.authenticated) {
session.out << "-ERR Not authenticated" << CRLF;
return 1;
}
request.skipWhitespace();
int msgNum = request.getTokenExclude(" ").asInteger();
MailFolder::Message *message = pop3FindMessage(session, msgNum);
if (!message) {
session.out << "-ERR No such message" << CRLF;
return 1;
}
session.out << "+OK " << (int)message->size << " octets" << CRLF;
pop3SendMessage(session, session.mailbox.readMessage(message->uid), -1);
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __POP3_RETR : public POP3Command {
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) override;
};
+6 -2
View File
@@ -7,8 +7,12 @@ namespace mail {
class __POP3_RPOP : public POP3Command {
int processCommand(coreutils::ZString &request, POP3Session &session, POP3Server &server) {
session.out << "250 OK" << CRLF;
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) override {
// Still a placeholder: this command dispatches and replies with a
// syntactically valid POP3 response now ("+OK", not the SMTP-style
// "250 OK" this used to send), but does not yet do any real
// mailbox work.
session.out << "+OK" << CRLF;
return 1;
}
+10 -2
View File
@@ -7,8 +7,16 @@ namespace mail {
class __POP3_RSET : public POP3Command {
int processCommand(coreutils::ZString &request, POP3Session &session, POP3Server &server) {
session.out << "250 OK" << CRLF;
// RFC 1939 5: unmarks every message DELE marked this session -
// does not undo anything already committed (nothing is committed
// until QUIT).
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) override {
if (!session.authenticated) {
session.out << "-ERR Not authenticated" << CRLF;
return 1;
}
session.deletedMarks.clear();
session.out << "+OK" << CRLF;
return 1;
}
+24
View File
@@ -0,0 +1,24 @@
#include "__POP3_STAT.h"
#include "POP3Server.h"
namespace mail {
int __POP3_STAT::processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) {
if (!session.authenticated) {
session.out << "-ERR Not authenticated" << CRLF;
return 1;
}
int count = 0;
long totalSize = 0;
const auto &messages = session.mailbox.messages();
for (size_t i = 0; i < messages.size(); ++i) {
if (session.deletedMarks.count((int)(i + 1)) > 0)
continue;
++count;
totalSize += (long)messages[i].size;
}
session.out << "+OK " << count << " " << totalSize << CRLF;
return 1;
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ namespace mail {
class __POP3_STAT : public POP3Command {
int processCommand(coreutils::ZString &request, POP3Session &session, POP3Server &server) {
session.out << "250 OK" << CRLF;
return 1;
}
int processCommand(coreutils::ZString request, POP3Session &session, POP3Server &server) override;
};
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more