- 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>
188 lines
7.8 KiB
C++
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;
|
|
}
|
|
|
|
}
|
|
|
|
}
|