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:
1 parent
641368ea85
commit
cd5e0a9905
122 files changed
+6059
-570
No files matched your search
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
+265
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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 ©) : 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
@@ -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 ©);
|
||||
~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
@@ -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 ©) : 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
@@ -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 ©);
|
||||
~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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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 §ionSpec) {
|
||||
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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
Reference in new issue
Block a user