Files
Brad ArantandClaude Sonnet 5 407cc2e915 Add per-domain DKIM signing, admin console TLS, log rotation, and resolver timeout scoping
- DKIM signing was hardcoded to one domain; now keyed per-domain off
  the sending address, fixing mail for every domain but the original.
- Admin console (port 1027) is now TLS-wrapped instead of plaintext.
- barantmail.log now rotates by size (config-driven) instead of
  growing unbounded (previously hit 23GB).
- ResolverThreadScope.h: a <setenv> scoping fix for name resolution
  inside a <try> block that previously did not survive its own
  container.
- IMAPArgs.cpp: fixed "UID STORE 1:*" wrongly requiring UID 1 to
  exist, which silently made empty-folder operations delete nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 17:39:06 +00:00

188 lines
7.8 KiB
C++

#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 if (uidMode) {
// A UID range's boundaries are just numbers, not required
// to name an existing message's UID - RFC 3501: "A:B"
// means every message whose UID falls in [A,B], not "the
// two messages literally named A and B". resolveNumber()
// above requires an exact existing-UID match via
// folder.find(), which is correct for a bare single-UID
// reference but wrong here: a real, long-lived mailbox
// almost never has a message with UID exactly 1 once
// anything has ever been expunged, so the standard
// "select everything" idiom "UID STORE 1:* +FLAGS
// (\\Deleted)" (used by the admin empty-folder feature)
// resolved fromUid to 0 and silently skipped the whole
// range every time - confirmed live: it reported
// {"ok":true} while deleting nothing from a real
// 538-message Trash folder. Scanning directly by UID
// instead of resolving each boundary to a sequence
// position first sidesteps that entirely.
std::string fromStr = part.substr(0, colon);
std::string toStr = part.substr(colon + 1);
uint32_t fromUid = (fromStr == "*") ? (folder.size() == 0 ? 0 : folder.messages().back().uid)
: (uint32_t)std::strtoul(fromStr.c_str(), nullptr, 10);
uint32_t toUid = (toStr == "*") ? (folder.size() == 0 ? 0 : folder.messages().back().uid)
: (uint32_t)std::strtoul(toStr.c_str(), nullptr, 10);
if (fromUid > toUid)
std::swap(fromUid, toUid);
for (const auto &message : folder.messages())
if (message.uid >= fromUid && message.uid <= toUid)
result.push_back(message.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);
// Sequence-number range (plain, non-UID STORE/FETCH/etc.) -
// resolveNumber() already turned both ends into concrete
// UIDs, and unlike the uidMode branch above, a plain
// sequence number always names a real position
// (1..folder.size()), so this round-trip through
// folder.seq() doesn't have that branch's problem. Walking
// sequence-number-by-sequence-number (not uid-by-uid) is
// still deliberate here: a 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;
}
}
}