#include "IMAPArgs.h" #include "MailFolder.h" #include #include #include #include #include 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 parseSequenceSet(coreutils::ZString &request, MailFolder &folder, bool uidMode) { std::vector 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 parseFlagList(coreutils::ZString &request) { coreutils::ZString arg = nextArgument(request); std::string text = unwrap(arg); std::vector 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; } } }