diff --git a/.gitignore b/.gitignore
index a7aef4a..7f9872d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,5 @@ jetc/libCoreUtils_pic.a
jetc/barant-site/jet/vst.jet
jetc/barant-site/jet/modular1.jet
jetc/barant-site/jet/modular1_patches.jet
+jetc/**/*.jtc
+jetc/__pycache__/
diff --git a/Tag.cpp b/Tag.cpp
index 2e9c2ef..a32c3eb 100644
--- a/Tag.cpp
+++ b/Tag.cpp
@@ -11,6 +11,11 @@
#include "__imapmessage.h"
#include "__imapflag.h"
#include "__imapdelete.h"
+#include "__imapmove.h"
+#include "__imapfolders.h"
+#include "__imapsearch.h"
+#include "__smtpsend.h"
+#include "__imapaccounts.h"
#include "__whilerow.h"
#include "__comment.h"
#include "__eval.h"
@@ -472,6 +477,21 @@ namespace jet {
} else if(ifTagName(in, "imapdelete")) {
__imapdelete _imapdelete(in, out, global, this, local);
continue;
+ } else if(ifTagName(in, "imapmove")) {
+ __imapmove _imapmove(in, out, global, this, local);
+ continue;
+ } else if(ifTagName(in, "imapfolders")) {
+ __imapfolders _imapfolders(in, out, global, this, local);
+ continue;
+ } else if(ifTagName(in, "imapsearch")) {
+ __imapsearch _imapsearch(in, out, global, this, local);
+ continue;
+ } else if(ifTagName(in, "smtpsend")) {
+ __smtpsend _smtpsend(in, out, global, this, local);
+ continue;
+ } else if(ifTagName(in, "imapaccounts")) {
+ __imapaccounts _imapaccounts(in, out, global, this, local);
+ continue;
} else if(ifTagName(in, "whilerow")) {
__whilerow _whilerow(in, out, global, this, local);
continue;
diff --git a/__eval.cpp b/__eval.cpp
new file mode 100644
index 0000000..66328ec
--- /dev/null
+++ b/__eval.cpp
@@ -0,0 +1,43 @@
+#include "__eval.h"
+#include "Exception.h"
+
+namespace jet {
+
+ // is not a jet-2.0 addition - it's a genuine jet-1.4 tag that was
+ // simply never implemented in 2.0 at all (no class, no dispatch entry
+ // anywhere). SESSIONCTL.tag's own definition depends on it
+ // throughout - capturing a tag's own invocation content into a named
+ // variable ( , often with
+ // scope="parent" so a nested custom tag can write back into its
+ // caller's scope: ...), and also
+ // using it bare (no name=) to render an 'd file's content
+ // inline ( ). Without this tag,
+ // all of that content was silently falling through as undefined-tag
+ // literal text, which is why a real, unmodified SESSIONCTL.tag
+ // never produced its actual rendered body.
+ //
+ // content - same
+ // capture semantics as content (Tag::storeVariable
+ // already implements exactly this: interpret the container, then store
+ // the result - raw if the container itself specified eval="no", already
+ // evaluated otherwise - into variable X at the requested scope,
+ // defaulting to global like does). Reused directly rather than
+ // reimplemented, since it's the identical contract under a different
+ // tag name.
+ //
+ // content (no name=) - content is interpreted and flows to
+ // normal output, exactly like any ordinary container-having tag; no
+ // special-casing needed since output/evaluate already default to true
+ // on the base Tag class and the destructor's copyContainer(out,
+ // parentOut) handles the rest once processContainer(container) below
+ // has populated `out`.
+ __eval::__eval(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
+ if(keywordDefined("name")) {
+ storeVariable(resolveKeyword("name"));
+ output = false;
+ } else if(hasContainer) {
+ processContainer(container);
+ }
+ }
+
+}
diff --git a/__eval.h b/__eval.h
new file mode 100644
index 0000000..aa0976f
--- /dev/null
+++ b/__eval.h
@@ -0,0 +1,21 @@
+#ifndef ____eval_h__
+#define ____eval_h__
+
+#include "Tag.h"
+#include "ZString.h"
+#include "MString.h"
+
+namespace jet {
+
+ class __eval : public Tag {
+
+ public:
+ __eval(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
+
+ protected:
+
+ };
+
+}
+
+#endif
diff --git a/__imap.cpp b/__imap.cpp
new file mode 100644
index 0000000..2b5edb4
--- /dev/null
+++ b/__imap.cpp
@@ -0,0 +1,1100 @@
+#include "__imap.h"
+#include "Base64.h"
+#include "Exception.h"
+#include "JFile.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace jet {
+
+ namespace {
+
+ struct PooledImapConnection {
+ SSL_CTX *ctx;
+ SSL *ssl;
+ int fd;
+ std::string host, user;
+ time_t lastUsed;
+ };
+ // thread_local, keyed by "sessionid|user" - same reasoning as
+ // __mysql.cpp's own connectionPool: this APIServer's EPoll worker
+ // pool is a fixed set of OS threads for the process's whole life,
+ // so a thread_local pool here is only ever touched by the one
+ // thread that created each entry, never concurrently.
+ //
+ // That thread-locality is exactly why an explicit disconnect
+ // (see the disconnect="yes" branch in __imap's constructor, and
+ // email.jet's /email/disconnect route) can't be relied on to
+ // reliably reach a given connection at all: EPoll's fixed worker
+ // pool is 4 OS threads, and there is no guarantee two requests
+ // from the same site session - a /email/connect and a later
+ // /email/disconnect - land on the SAME one. A disconnect that
+ // happens to run on a different thread than the one holding the
+ // live connection finds nothing in ITS OWN, empty-for-this-key
+ // pool and silently does nothing, leaving the real connection on
+ // the other thread open and unread indefinitely (confirmed live -
+ // see [[barantcom-email-page-fixes-20260904]]). The idle-sweep
+ // below is the actual guarantee: it runs at the top of EVERY
+ // construction, on whichever thread that happens to be,
+ // so any thread that ever handles IMAP traffic again eventually
+ // cleans out its own stale entries regardless of which thread
+ // any particular disconnect call landed on. The explicit
+ // disconnect="yes" close is kept too, as a same-thread fast path
+ // when it does happen to line up - just not the only mechanism.
+ thread_local std::unordered_map imapPool;
+
+ const time_t IMAP_POOL_IDLE_TIMEOUT_SECONDS = 120;
+
+ void closePooledConnection(PooledImapConnection &c) {
+ if (c.ssl) { SSL_shutdown(c.ssl); SSL_free(c.ssl); }
+ if (c.fd >= 0) ::close(c.fd);
+ if (c.ctx) SSL_CTX_free(c.ctx);
+ }
+
+ // Called at the top of every construction (connect or
+ // disconnect alike) - closes and evicts any pooled connection on
+ // THIS thread that's sat unused past the idle timeout, regardless
+ // of which user/session it belongs to. This is what actually
+ // bounds how long a leaked connection (an explicit disconnect
+ // that missed it, a browser tab closed mid-session, a crashed
+ // client) can stay open, independent of whether any future
+ // request ever happens to target that exact user again.
+ void sweepIdleImapConnections() {
+ time_t now = time(nullptr);
+ for (auto it = imapPool.begin(); it != imapPool.end(); ) {
+ if (now - it->second.lastUsed > IMAP_POOL_IDLE_TIMEOUT_SECONDS) {
+ closePooledConnection(it->second);
+ it = imapPool.erase(it);
+ } else {
+ ++it;
+ }
+ }
+ }
+
+ std::string imapQuote(const std::string &s) {
+ std::string out = "\"";
+ for (char c : s) {
+ if (c == '"' || c == '\\')
+ out += '\\';
+ out += c;
+ }
+ out += "\"";
+ return out;
+ }
+
+ std::string trimWS(std::string s) {
+ size_t a = s.find_first_not_of(" \t");
+ size_t b = s.find_last_not_of(" \t");
+ return (a == std::string::npos) ? std::string() : s.substr(a, b - a + 1);
+ }
+
+ bool taggedOk(const std::vector &lines, const std::string &tag) {
+ return !lines.empty() && lines.back().rfind(tag + " OK", 0) == 0;
+ }
+
+ // Given `line` (a literal-rejoined response string - see
+ // __imap::readResponseLine()'s own doc comment) and the index of
+ // the '}' that closes a "{n}" literal-length marker somewhere in
+ // it, returns the n bytes immediately following (skipping the
+ // "\r\n" joiner readResponseLine() inserted there).
+ std::string extractLiteralAt(const std::string &line, size_t braceEndPos) {
+ size_t braceStart = line.rfind('{', braceEndPos);
+ if (braceStart == std::string::npos)
+ return "";
+ std::string numStr = line.substr(braceStart + 1, braceEndPos - braceStart - 1);
+ if (numStr.empty() || numStr.find_first_not_of("0123456789") != std::string::npos)
+ return "";
+ long n = strtol(numStr.c_str(), nullptr, 10);
+ size_t contentStart = braceEndPos + 1;
+ if (contentStart + 2 <= line.size() && line[contentStart] == '\r' && line[contentStart + 1] == '\n')
+ contentStart += 2;
+ if (contentStart > line.size())
+ return "";
+ if (contentStart + (size_t)n > line.size())
+ n = (long)(line.size() - contentStart);
+ return line.substr(contentStart, n);
+ }
+
+ // Q-encoding (RFC 2047's variant of quoted-printable used inside
+ // "=?charset?Q?...?=" encoded-words) - '_' stands for a literal
+ // space (plain quoted-printable would need "=20" for that) and
+ // "=XX" is a hex-escaped byte, same as body quoted-printable.
+ std::string decodeQEncoding(const std::string &in) {
+ std::string out;
+ out.reserve(in.size());
+ for (size_t i = 0; i < in.size(); ++i) {
+ char c = in[i];
+ if (c == '_') {
+ out += ' ';
+ } else if (c == '=' && i + 2 < in.size() &&
+ isxdigit((unsigned char)in[i + 1]) && isxdigit((unsigned char)in[i + 2])) {
+ out += (char)strtol(in.substr(i + 1, 2).c_str(), nullptr, 16);
+ i += 2;
+ } else {
+ out += c;
+ }
+ }
+ return out;
+ }
+
+ // Real-world mail overwhelmingly declares UTF-8 (every encoded
+ // subject seen from actual traffic to this mailbox does), so that
+ // case is a pure passthrough; anything else goes through iconv,
+ // the standard POSIX charset-conversion API, falling back to the
+ // raw bytes if the declared charset is one iconv doesn't
+ // recognize rather than throwing - a garbled subject is a far
+ // better failure mode than a 500 on the whole message list.
+ std::string convertToUtf8(const std::string &bytes, const std::string &charset) {
+ std::string cs = charset;
+ std::transform(cs.begin(), cs.end(), cs.begin(), [](unsigned char c) { return std::tolower(c); });
+ if (cs.empty() || cs == "utf-8" || cs == "us-ascii")
+ return bytes;
+ iconv_t cd = iconv_open("UTF-8//IGNORE", charset.c_str());
+ if (cd == (iconv_t)-1)
+ return bytes;
+ std::string result(bytes.size() * 4 + 16, '\0');
+ char *inBuf = const_cast(bytes.data());
+ size_t inBytesLeft = bytes.size();
+ char *outStart = &result[0];
+ char *outBuf = outStart;
+ size_t outBytesLeft = result.size();
+ iconv(cd, &inBuf, &inBytesLeft, &outBuf, &outBytesLeft);
+ iconv_close(cd);
+ if (outBuf == outStart)
+ return bytes;
+ result.resize(outBuf - outStart);
+ return result;
+ }
+
+ // Decodes RFC 2047 MIME encoded-words ("=?charset?B|Q?text?=")
+ // that show up verbatim in raw header field values - IMAP hands
+ // Subject/From/To back exactly as the sender wrote them, with no
+ // decoding of its own, so without this every non-ASCII subject
+ // (extremely common in real mail - every Japanese-language
+ // message seen in this mailbox uses it) rendered as raw
+ // "=?UTF-8?B?...?=" garbage in the webmail UI instead of the
+ // actual text. Per RFC 2047 section 2, whitespace that sits
+ // ONLY between two consecutive encoded-words is itself part of
+ // the encoding (a folding artifact) and gets dropped; whitespace
+ // next to plain text is preserved as-is.
+ std::string decodeMimeHeader(const std::string &raw) {
+ std::string result;
+ size_t pos = 0;
+ bool lastWasEncoded = false;
+ while (pos < raw.size()) {
+ size_t start = raw.find("=?", pos);
+ if (start == std::string::npos) {
+ result += raw.substr(pos);
+ break;
+ }
+
+ std::string gap = raw.substr(pos, start - pos);
+ bool gapIsWhitespaceOnly = gap.find_first_not_of(" \t\r\n") == std::string::npos;
+ if (!(lastWasEncoded && gapIsWhitespaceOnly))
+ result += gap;
+
+ size_t q1 = raw.find('?', start + 2);
+ bool valid = (q1 != std::string::npos) && (q1 - start - 2 <= 40) && (q1 - start - 2 >= 1);
+ size_t q2 = valid ? raw.find('?', q1 + 1) : std::string::npos;
+ valid = valid && (q2 == q1 + 2);
+ char encLetter = valid ? (char)toupper((unsigned char)raw[q1 + 1]) : 0;
+ valid = valid && (encLetter == 'B' || encLetter == 'Q');
+ size_t end = valid ? raw.find("?=", q2 + 1) : std::string::npos;
+ valid = valid && (end != std::string::npos);
+
+ if (!valid) {
+ result += raw.substr(start, 2);
+ pos = start + 2;
+ lastWasEncoded = false;
+ continue;
+ }
+
+ std::string charset = raw.substr(start + 2, q1 - start - 2);
+ std::string encodedText = raw.substr(q2 + 1, end - q2 - 1);
+
+ std::string decodedBytes;
+ if (encLetter == 'B') {
+ coreutils::Base64 b64;
+ decodedBytes = b64.decode(coreutils::ZString(encodedText.c_str(), encodedText.length())).str();
+ } else {
+ decodedBytes = decodeQEncoding(encodedText);
+ }
+ result += convertToUtf8(decodedBytes, charset);
+
+ pos = end + 2;
+ lastWasEncoded = true;
+ }
+ return result;
+ }
+
+ void parseHeaderFields(const std::string &headerBlock, std::string &from, std::string &to,
+ std::string &subject, std::string &date) {
+ size_t pos = 0;
+ while (pos < headerBlock.size()) {
+ size_t nl = headerBlock.find("\r\n", pos);
+ std::string hline = (nl == std::string::npos) ? headerBlock.substr(pos) : headerBlock.substr(pos, nl - pos);
+ if (hline.rfind("From:", 0) == 0) from = hline.substr(5);
+ else if (hline.rfind("To:", 0) == 0) to = hline.substr(3);
+ else if (hline.rfind("Subject:", 0) == 0) subject = hline.substr(8);
+ else if (hline.rfind("Date:", 0) == 0) date = hline.substr(5);
+ if (nl == std::string::npos)
+ break;
+ pos = nl + 2;
+ }
+ }
+
+ // RFC 2822 header unfolding - a continuation line (one starting
+ // with a space or tab) is really a soft-wrapped part of the
+ // PREVIOUS field's value, not a new one. Real mail wraps long
+ // Content-Type lines this way constantly (a multipart boundary
+ // string alone is often 40+ characters), so without unfolding
+ // first, a plain per-line scan for "boundary=" would miss it
+ // whenever it landed on its own continuation line.
+ std::string unfoldHeaders(const std::string &s) {
+ std::string out;
+ size_t i = 0;
+ while (i < s.size()) {
+ if (s[i] == '\r' && i + 2 < s.size() && s[i + 1] == '\n' && (s[i + 2] == ' ' || s[i + 2] == '\t')) {
+ out += ' ';
+ i += 3;
+ while (i < s.size() && (s[i] == ' ' || s[i] == '\t'))
+ ++i;
+ } else {
+ out += s[i];
+ ++i;
+ }
+ }
+ return out;
+ }
+
+ // Case-insensitive header lookup against an already-unfolded
+ // header block - Content-Type/Content-Transfer-Encoding/Content-
+ // Disposition all show up in whatever case the sending client
+ // happened to use, unlike From/To/Subject/Date above which real
+ // mail overwhelmingly capitalizes consistently.
+ std::string getHeaderValueCI(const std::string &unfolded, const std::string &name) {
+ size_t pos = 0;
+ while (pos < unfolded.size()) {
+ size_t nl = unfolded.find("\r\n", pos);
+ std::string line = (nl == std::string::npos) ? unfolded.substr(pos) : unfolded.substr(pos, nl - pos);
+ size_t colon = line.find(':');
+ if (colon != std::string::npos && colon == name.size()) {
+ bool match = true;
+ for (size_t i = 0; i < name.size() && match; ++i)
+ match = tolower((unsigned char)line[i]) == tolower((unsigned char)name[i]);
+ if (match)
+ return trimWS(line.substr(colon + 1));
+ }
+ if (nl == std::string::npos)
+ break;
+ pos = nl + 2;
+ }
+ return "";
+ }
+
+ // The bare "type/subtype" portion of a header value like
+ // "multipart/alternative; boundary=\"...\"" or "text/html;
+ // charset=UTF-8" - lowercased, since MIME type/subtype names are
+ // case-insensitive by spec and real senders vary in practice.
+ std::string getMainValue(const std::string &headerValue) {
+ size_t semi = headerValue.find(';');
+ std::string main = (semi == std::string::npos) ? headerValue : headerValue.substr(0, semi);
+ main = trimWS(main);
+ std::transform(main.begin(), main.end(), main.begin(), [](unsigned char c) { return std::tolower(c); });
+ return main;
+ }
+
+ // A parameter off a header value, e.g. boundary= or charset= -
+ // quoted ("...") or bare, matched case-insensitively by name but
+ // returned with its original case intact (a charset name or a
+ // boundary string both matter verbatim).
+ std::string getParam(const std::string &headerValue, const std::string ¶mName) {
+ std::string lowerVal = headerValue;
+ std::transform(lowerVal.begin(), lowerVal.end(), lowerVal.begin(), [](unsigned char c) { return std::tolower(c); });
+ std::string needle = paramName + "=";
+ size_t pos = lowerVal.find(needle);
+ if (pos == std::string::npos)
+ return "";
+ size_t valStart = pos + needle.size();
+ if (valStart >= headerValue.size())
+ return "";
+ if (headerValue[valStart] == '"') {
+ size_t endQuote = headerValue.find('"', valStart + 1);
+ if (endQuote == std::string::npos)
+ return "";
+ return headerValue.substr(valStart + 1, endQuote - valStart - 1);
+ }
+ size_t end = headerValue.find_first_of("; \t\r\n", valStart);
+ return (end == std::string::npos) ? headerValue.substr(valStart) : headerValue.substr(valStart, end - valStart);
+ }
+
+ // Body quoted-printable - distinct from decodeQEncoding above
+ // (RFC 2047's own "=XX plus '_' for space" variant used only
+ // inside header encoded-words): a message BODY uses plain RFC
+ // 2045 quoted-printable, where "=\r\n" (or a bare "=\n") is a
+ // soft line break to be removed entirely, not a literal
+ // character, and there is no '_'-for-space special case at all.
+ std::string decodeQuotedPrintableBody(const std::string &in) {
+ std::string out;
+ out.reserve(in.size());
+ for (size_t i = 0; i < in.size(); ++i) {
+ char c = in[i];
+ if (c != '=') {
+ out += c;
+ continue;
+ }
+ if (i + 2 < in.size() && in[i + 1] == '\r' && in[i + 2] == '\n') {
+ i += 2;
+ } else if (i + 1 < in.size() && in[i + 1] == '\n') {
+ i += 1;
+ } else if (i + 2 < in.size() && isxdigit((unsigned char)in[i + 1]) && isxdigit((unsigned char)in[i + 2])) {
+ out += (char)strtol(in.substr(i + 1, 2).c_str(), nullptr, 16);
+ i += 2;
+ } else {
+ out += c;
+ }
+ }
+ return out;
+ }
+
+ struct MimeTextPart {
+ std::string decodedBody;
+ };
+
+ // Walks a MIME part (given as separate header/body blocks - the
+ // top-level message the first time, one boundary-delimited
+ // sub-part on every recursive call after that) looking for the
+ // best text/html and text/plain content to show as a message
+ // body. Recurses into any multipart/* container uniformly
+ // (alternative, mixed, related - all handled the same way,
+ // which correctly falls through nested wrappers like a
+ // multipart/mixed enclosing a multipart/alternative without
+ // needing to special-case either), skips anything marked
+ // Content-Disposition: attachment, and keeps only the FIRST
+ // text/html and FIRST text/plain part found (document order) -
+ // deliberately not the most sophisticated possible MIME walker,
+ // but real mail practically never has more than one of each at
+ // the "body" level, and this matches the same pragmatic,
+ // purpose-built philosophy as the rest of this class (see
+ // __imap.h's own doc comment on readResponseLine()).
+ void collectBestTextParts(const std::string &headerBlock, const std::string &bodyBlock,
+ MimeTextPart &htmlOut, bool &haveHtml, MimeTextPart &plainOut, bool &havePlain) {
+ std::string unfolded = unfoldHeaders(headerBlock);
+ std::string contentTypeHeader = getHeaderValueCI(unfolded, "Content-Type");
+ std::string mainType = contentTypeHeader.empty() ? "text/plain" : getMainValue(contentTypeHeader);
+
+ if (mainType.rfind("multipart/", 0) == 0) {
+ std::string boundary = getParam(contentTypeHeader, "boundary");
+ if (boundary.empty())
+ return;
+ std::string delim = "--" + boundary;
+ size_t pos = bodyBlock.find(delim);
+ while (pos != std::string::npos) {
+ size_t partStart = pos + delim.size();
+ if (partStart + 1 < bodyBlock.size() && bodyBlock[partStart] == '-' && bodyBlock[partStart + 1] == '-')
+ break;
+ if (partStart < bodyBlock.size() && bodyBlock[partStart] == '\r')
+ partStart += 2;
+ else if (partStart < bodyBlock.size() && bodyBlock[partStart] == '\n')
+ partStart += 1;
+
+ size_t nextPos = bodyBlock.find(delim, partStart);
+ std::string partRaw = (nextPos == std::string::npos)
+ ? bodyBlock.substr(partStart)
+ : bodyBlock.substr(partStart, nextPos - partStart);
+
+ size_t hEnd = partRaw.find("\r\n\r\n");
+ std::string partHeader = (hEnd == std::string::npos) ? partRaw : partRaw.substr(0, hEnd);
+ std::string partBody = (hEnd == std::string::npos) ? "" : partRaw.substr(hEnd + 4);
+
+ collectBestTextParts(partHeader, partBody, htmlOut, haveHtml, plainOut, havePlain);
+
+ if (nextPos == std::string::npos)
+ break;
+ pos = nextPos;
+ }
+ return;
+ }
+
+ std::string disposition = getMainValue(getHeaderValueCI(unfolded, "Content-Disposition"));
+ if (disposition == "attachment")
+ return;
+ if (mainType != "text/html" && mainType != "text/plain")
+ return;
+ if ((mainType == "text/html" && haveHtml) || (mainType == "text/plain" && havePlain))
+ return;
+
+ std::string transferEncoding = getMainValue(getHeaderValueCI(unfolded, "Content-Transfer-Encoding"));
+ std::string decoded;
+ if (transferEncoding == "base64") {
+ std::string stripped;
+ stripped.reserve(bodyBlock.size());
+ for (char c : bodyBlock)
+ if (!isspace((unsigned char)c))
+ stripped += c;
+ coreutils::Base64 b64;
+ decoded = b64.decode(coreutils::ZString(stripped.c_str(), stripped.length())).str();
+ } else if (transferEncoding == "quoted-printable") {
+ decoded = decodeQuotedPrintableBody(bodyBlock);
+ } else {
+ decoded = bodyBlock;
+ }
+
+ decoded = convertToUtf8(decoded, getParam(contentTypeHeader, "charset"));
+
+ if (mainType == "text/html") {
+ htmlOut.decodedBody = decoded;
+ haveHtml = true;
+ } else {
+ plainOut.decodedBody = decoded;
+ havePlain = true;
+ }
+ }
+
+ }
+
+ // Shared by listMessages() and searchMessages() - both issue the
+ // identical FETCH field list (UID FLAGS RFC822.SIZE
+ // BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)]), just against a
+ // different sequence set (1:* vs whatever UIDs a SEARCH turned
+ // up), so this is the one place that turns those response lines
+ // into the JSON summary objects the webmail client's message list
+ // already knows how to render. A private static member (not a
+ // plain anonymous-namespace function) specifically so it can call
+ // findUidField() and friends directly - those are private statics
+ // of this same class.
+ std::vector __imap::parseSummaryLines(const std::vector &lines) {
+ std::vector items;
+ for (auto &line : lines) {
+ if (line.rfind("* ", 0) != 0 || line.find("FETCH") == std::string::npos)
+ continue;
+
+ uint32_t uid = findUidField(line);
+ if (uid == 0)
+ continue;
+ std::vector flags = findFlagsField(line);
+ long size = findSizeField(line);
+ std::string headerBlock = findBodyLiteral(line);
+
+ std::string from, to, subject, date;
+ parseHeaderFields(headerBlock, from, to, subject, date);
+ from = decodeMimeHeader(from);
+ subject = decodeMimeHeader(subject);
+
+ bool seen = std::find(flags.begin(), flags.end(), "\\Seen") != flags.end();
+ bool flagged = std::find(flags.begin(), flags.end(), "\\Flagged") != flags.end();
+
+ std::ostringstream item;
+ item << "{\"uid\":" << uid
+ << ",\"from\":\"" << jsonEscape(trimWS(from)) << "\""
+ << ",\"subject\":\"" << jsonEscape(trimWS(subject)) << "\""
+ << ",\"date\":\"" << jsonEscape(trimWS(date)) << "\""
+ << ",\"size\":" << size
+ << ",\"seen\":" << (seen ? "true" : "false")
+ << ",\"flagged\":" << (flagged ? "true" : "false")
+ << "}";
+ items.push_back(item.str());
+ }
+ return items;
+ }
+
+ __imap::__imap(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, this) {
+
+ if (!keywordDefined("config"))
+ throw coreutils::Exception("config must be specified for imap tag.", __FILE__, __LINE__, 500);
+ if (!keywordDefined("user"))
+ throw coreutils::Exception("user must be specified for imap tag.", __FILE__, __LINE__, 401);
+
+ sweepIdleImapConnections();
+
+ sessionId = resolveKeyword("sessionid");
+ coreutils::MString userVal = resolveKeyword("user");
+
+ // A disconnect="yes" invocation only tears down this user's own
+ // pooled connection (see the reuse-pooled-connection branch just
+ // below, and email.jet's /email/disconnect route) - real IMAP
+ // servers keep pushing protocol state at an open connection for
+ // as long as it stays open, whether or not this process is still
+ // reading it. /email/disconnect used to just clear the site
+ // session's own _imap_user/_imap_password variables and leave
+ // the real, pooled TCP+TLS socket sitting open and completely
+ // unread forever (the destructor deliberately never closes it -
+ // see its own doc comment - by design, for reuse across requests
+ // on the same worker thread, but nothing ever ended that reuse
+ // window on an explicit disconnect). BARANTMail eventually tries
+ // to write something to one of these abandoned sockets and hits
+ // exactly the "stuck write retry loop" pattern
+ // [[barantmail-ssl-busyloop-diskfull]]'s own fix already catches
+ // and shuts down correctly - but repeatedly, once per leaked
+ // connection, which is what was actually producing the ongoing
+ // log growth (and quite possibly feeding whatever's triggering
+ // the separate double-free crashes seen the same day - a shutdown
+ // path getting exercised this often, on connections nothing else
+ // in this process still references, is exactly the kind of
+ // pattern that turns a latent use-after-free into a frequent one).
+ if (keywordDefined("disconnect") && resolveKeyword("disconnect") == "yes") {
+ std::string poolKey = sessionId.str() + "|" + userVal.str();
+ auto pooled = imapPool.find(poolKey);
+ if (pooled != imapPool.end()) {
+ closePooledConnection(pooled->second);
+ imapPool.erase(pooled);
+ }
+ return;
+ }
+
+ if (!keywordDefined("password"))
+ throw coreutils::Exception("password must be specified for imap tag.", __FILE__, __LINE__, 401);
+
+ coreutils::MString configPath = resolveKeyword("config");
+ coreutils::MString passwordVal = resolveKeyword("password");
+
+ coreutils::JFile config(configPath);
+ std::string host = config.getString("host");
+ long port = (long)config.getNumber("port");
+ if (port <= 0)
+ port = 993;
+
+ std::string poolKey = sessionId.str() + "|" + userVal.str();
+ auto pooled = imapPool.find(poolKey);
+ if (pooled != imapPool.end() && pooled->second.host == host && pooled->second.user == userVal.str()) {
+ ctx = pooled->second.ctx;
+ ssl = pooled->second.ssl;
+ fd = pooled->second.fd;
+ imapPool.erase(pooled);
+
+ bool alive = true;
+ try {
+ std::string tag = nextTag();
+ sendLine(tag + " NOOP");
+ auto lines = readUntilTagged(tag);
+ alive = taggedOk(lines, tag);
+ } catch (...) {
+ alive = false;
+ }
+ if (!alive) {
+ if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); }
+ if (fd >= 0) ::close(fd);
+ if (ctx) SSL_CTX_free(ctx);
+ ssl = nullptr;
+ fd = -1;
+ ctx = nullptr;
+ recvBuffer.clear();
+ recvPos = 0;
+ }
+ }
+
+ if (!ssl) {
+ struct addrinfo hints {};
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ struct addrinfo *result = nullptr;
+ std::string portStr = std::to_string(port);
+ if (getaddrinfo(host.c_str(), portStr.c_str(), &hints, &result) != 0 || !result)
+ throw coreutils::Exception("imap: could not resolve " + host, __FILE__, __LINE__, 502);
+
+ int newFd = -1;
+ for (struct addrinfo *rp = result; rp != nullptr; rp = rp->ai_next) {
+ newFd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ if (newFd < 0)
+ continue;
+ if (::connect(newFd, rp->ai_addr, rp->ai_addrlen) == 0)
+ break;
+ ::close(newFd);
+ newFd = -1;
+ }
+ freeaddrinfo(result);
+ if (newFd < 0)
+ throw coreutils::Exception("imap: could not connect to " + host, __FILE__, __LINE__, 502);
+
+ struct timeval timeout { 30, 0 };
+ setsockopt(newFd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
+ setsockopt(newFd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
+
+ SSL_CTX *newCtx = SSL_CTX_new(SSLv23_client_method());
+ if (!newCtx) {
+ ::close(newFd);
+ throw coreutils::Exception("imap: SSL_CTX_new failed", __FILE__, __LINE__, 502);
+ }
+ SSL *newSsl = SSL_new(newCtx);
+ SSL_set_fd(newSsl, newFd);
+ if (SSL_connect(newSsl) != 1) {
+ SSL_free(newSsl);
+ SSL_CTX_free(newCtx);
+ ::close(newFd);
+ throw coreutils::Exception("imap: TLS handshake failed", __FILE__, __LINE__, 502);
+ }
+
+ ctx = newCtx;
+ ssl = newSsl;
+ fd = newFd;
+
+ // Consume the server's own greeting line ("* OK ...") before
+ // sending anything.
+ readResponseLine();
+
+ std::string tag = nextTag();
+ sendLine(tag + " LOGIN " + imapQuote(userVal.str()) + " " + imapQuote(passwordVal.str()));
+ auto lines = readUntilTagged(tag);
+ if (!taggedOk(lines, tag)) {
+ SSL_shutdown(ssl);
+ SSL_free(ssl);
+ SSL_CTX_free(ctx);
+ ::close(fd);
+ ssl = nullptr;
+ ctx = nullptr;
+ fd = -1;
+ throw coreutils::Exception("Invalid mailbox email or password.", __FILE__, __LINE__, 401);
+ }
+ }
+
+ imapPool[poolKey] = PooledImapConnection{ctx, ssl, fd, host, userVal.str(), time(nullptr)};
+
+ global.addImapSession(sessionId, this);
+ processContainer(container);
+ }
+
+ __imap::~__imap() {
+ global.removeImapSession(sessionId);
+ // ssl/fd/ctx deliberately NOT closed here - owned by this worker
+ // thread's persistent pool now, exactly like __mysql's own
+ // destructor leaves `mysql` alone. See the constructor's pooling
+ // logic above.
+ }
+
+ std::string __imap::nextTag() {
+ return "a" + std::to_string(++tagCounter);
+ }
+
+ void __imap::sendLine(const std::string &line) {
+ std::string data = line + "\r\n";
+ size_t sent = 0;
+ while (sent < data.size()) {
+ int n = SSL_write(ssl, data.data() + sent, (int)(data.size() - sent));
+ if (n <= 0)
+ throw coreutils::Exception("imap: connection lost while writing.", __FILE__, __LINE__, 502);
+ sent += n;
+ }
+ }
+
+ char __imap::readByte() {
+ while (recvPos >= recvBuffer.size()) {
+ char buf[8192];
+ int n = SSL_read(ssl, buf, sizeof(buf));
+ if (n <= 0)
+ throw coreutils::Exception("imap: connection lost while reading.", __FILE__, __LINE__, 502);
+ recvBuffer.append(buf, n);
+ }
+ return recvBuffer[recvPos++];
+ }
+
+ std::string __imap::readResponseLine() {
+ std::string line;
+ char c;
+ while (true) {
+ c = readByte();
+ if (c == '\r')
+ continue;
+ if (c == '\n')
+ break;
+ line += c;
+ }
+
+ if (!line.empty() && line.back() == '}') {
+ size_t brace = line.rfind('{');
+ if (brace != std::string::npos) {
+ std::string numStr = line.substr(brace + 1, line.size() - brace - 2);
+ bool allDigits = !numStr.empty() && numStr.find_first_not_of("0123456789") == std::string::npos;
+ if (allDigits) {
+ long n = strtol(numStr.c_str(), nullptr, 10);
+ std::string literalData;
+ literalData.reserve(n);
+ for (long i = 0; i < n; ++i)
+ literalData += readByte();
+ // The literal is always followed by the rest of the SAME
+ // logical response line (more items, then the real
+ // CRLF) - recursing into this same function reads that
+ // remainder, correctly handling a second literal on the
+ // same line (e.g. FETCH ... BODY[HEADER...] {n1} ...
+ // BODY[TEXT] {n2} ...) via a further recursion.
+ std::string rest = readResponseLine();
+ recvBuffer.erase(0, recvPos);
+ recvPos = 0;
+ return line + "\r\n" + literalData + "\r\n" + rest;
+ }
+ }
+ }
+
+ recvBuffer.erase(0, recvPos);
+ recvPos = 0;
+ return line;
+ }
+
+ std::vector __imap::readUntilTagged(const std::string &tag) {
+ std::vector lines;
+ std::string prefix = tag + " ";
+ while (true) {
+ std::string line = readResponseLine();
+ lines.push_back(line);
+ if (line.rfind(prefix, 0) == 0)
+ break;
+ }
+ return lines;
+ }
+
+ bool __imap::selectFolderIfNeeded(const std::string &folder) {
+ if (selectedFolder == folder)
+ return true;
+ std::string tag = nextTag();
+ sendLine(tag + " SELECT " + folder);
+ auto lines = readUntilTagged(tag);
+ if (!taggedOk(lines, tag))
+ return false;
+ selectedFolder = folder;
+ return true;
+ }
+
+ std::string __imap::jsonEscape(const std::string &s) {
+ std::string out;
+ out.reserve(s.size() + 8);
+ for (unsigned char c : s) {
+ switch (c) {
+ case '"': out += "\\\""; break;
+ case '\\': out += "\\\\"; break;
+ case '\n': out += "\\n"; break;
+ case '\r': continue;
+ case '\t': out += "\\t"; break;
+ default:
+ if (c < 0x20) {
+ char buf[8];
+ snprintf(buf, sizeof(buf), "\\u%04x", c);
+ out += buf;
+ } else {
+ out += (char)c;
+ }
+ }
+ }
+ return out;
+ }
+
+ uint32_t __imap::findUidField(const std::string &fetchLine) {
+ size_t pos = fetchLine.find("UID ");
+ if (pos == std::string::npos)
+ return 0;
+ return (uint32_t)strtoul(fetchLine.c_str() + pos + 4, nullptr, 10);
+ }
+
+ long __imap::findSizeField(const std::string &fetchLine) {
+ size_t pos = fetchLine.find("RFC822.SIZE ");
+ if (pos == std::string::npos)
+ return 0;
+ return strtol(fetchLine.c_str() + pos + 12, nullptr, 10);
+ }
+
+ std::vector __imap::findFlagsField(const std::string &fetchLine) {
+ std::vector flags;
+ size_t pos = fetchLine.find("FLAGS (");
+ if (pos == std::string::npos)
+ return flags;
+ size_t start = pos + 7;
+ size_t end = fetchLine.find(')', start);
+ if (end == std::string::npos)
+ return flags;
+ std::istringstream ss(fetchLine.substr(start, end - start));
+ std::string tok;
+ while (ss >> tok)
+ flags.push_back(tok);
+ return flags;
+ }
+
+ std::string __imap::findBodyLiteral(const std::string &fetchLine) {
+ size_t bodyPos = fetchLine.find("BODY[");
+ if (bodyPos == std::string::npos)
+ return "";
+ size_t bracketEnd = fetchLine.find(']', bodyPos);
+ if (bracketEnd == std::string::npos)
+ return "";
+ size_t braceStart = fetchLine.find('{', bracketEnd);
+ if (braceStart == std::string::npos)
+ return "";
+ size_t braceEnd = fetchLine.find('}', braceStart);
+ if (braceEnd == std::string::npos)
+ return "";
+ return extractLiteralAt(fetchLine, braceEnd);
+ }
+
+ coreutils::MString __imap::listMessages(const std::string &folder, int limit) {
+ if (!selectFolderIfNeeded(folder))
+ throw coreutils::Exception("imap: could not select folder " + folder, __FILE__, __LINE__, 502);
+
+ std::string tag = nextTag();
+ sendLine(tag + " FETCH 1:* (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])");
+ auto lines = readUntilTagged(tag);
+ if (!taggedOk(lines, tag))
+ throw coreutils::Exception("imap: FETCH failed", __FILE__, __LINE__, 502);
+
+ std::vector items = parseSummaryLines(lines);
+
+ // Keep only the newest `limit` - UIDs are assigned in strictly
+ // ascending delivery order (see MailFolder.h, BARANTMail, built
+ // earlier this session), so the tail of this UID-ascending list
+ // is the newest mail.
+ if ((int)items.size() > limit)
+ items.erase(items.begin(), items.end() - limit);
+ std::reverse(items.begin(), items.end());
+
+ std::string json = "[";
+ for (size_t i = 0; i < items.size(); ++i) {
+ if (i)
+ json += ",";
+ json += items[i];
+ }
+ json += "]";
+ return coreutils::MString(json);
+ }
+
+ // BARANTMail's own __IMAP_SEARCH.cpp supports FROM/SUBJECT/BODY and
+ // a chained OR, which is exactly enough to build "one query box
+ // matching sender, subject, or body content" without needing to run
+ // three separate searches and merge results by hand - OR takes
+ // exactly two search keys per IMAP4rev1 grammar, so a 3-way match
+ // is the same OR nested once: OR FROM OR SUBJECT BODY reads as
+ // FROM-matches OR (SUBJECT-matches OR BODY-matches).
+ coreutils::MString __imap::searchMessages(const std::string &folder, const std::string &query) {
+ if (!selectFolderIfNeeded(folder))
+ throw coreutils::Exception("imap: could not select folder " + folder, __FILE__, __LINE__, 502);
+
+ std::string escaped;
+ escaped.reserve(query.size());
+ for (char c : query) {
+ if (c == '"' || c == '\\')
+ escaped += '\\';
+ escaped += c;
+ }
+
+ std::string searchTag = nextTag();
+ sendLine(searchTag + " UID SEARCH OR FROM \"" + escaped + "\" OR SUBJECT \"" + escaped + "\" BODY \"" + escaped + "\"");
+ auto searchLines = readUntilTagged(searchTag);
+ if (!taggedOk(searchLines, searchTag))
+ throw coreutils::Exception("imap: SEARCH failed", __FILE__, __LINE__, 502);
+
+ std::vector uidStrings;
+ for (auto &line : searchLines) {
+ if (line.rfind("* SEARCH", 0) != 0)
+ continue;
+ std::istringstream iss(line.substr(8));
+ std::string tok;
+ while (iss >> tok)
+ uidStrings.push_back(tok);
+ }
+
+ if (uidStrings.empty())
+ return coreutils::MString("[]");
+
+ std::string seqSet;
+ for (size_t i = 0; i < uidStrings.size(); ++i) {
+ if (i)
+ seqSet += ",";
+ seqSet += uidStrings[i];
+ }
+
+ std::string fetchTag = nextTag();
+ sendLine(fetchTag + " UID FETCH " + seqSet + " (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])");
+ auto fetchLines = readUntilTagged(fetchTag);
+ if (!taggedOk(fetchLines, fetchTag))
+ throw coreutils::Exception("imap: FETCH failed", __FILE__, __LINE__, 502);
+
+ std::vector items = parseSummaryLines(fetchLines);
+ std::reverse(items.begin(), items.end());
+
+ std::string json = "[";
+ for (size_t i = 0; i < items.size(); ++i) {
+ if (i)
+ json += ",";
+ json += items[i];
+ }
+ json += "]";
+ return coreutils::MString(json);
+ }
+
+ coreutils::MString __imap::fetchMessage(const std::string &folder, uint32_t uid) {
+ if (!selectFolderIfNeeded(folder))
+ throw coreutils::Exception("imap: could not select folder " + folder, __FILE__, __LINE__, 502);
+
+ // A single BODY[] pulls the whole message (headers + every MIME
+ // part) in one literal - needed now to actually see Content-Type/
+ // boundary/Content-Transfer-Encoding, none of which the old
+ // HEADER.FIELDS-subset + BODY[TEXT] split ever exposed, so the
+ // raw MIME envelope (boundaries, base64/quoted-printable
+ // encoded parts, nested multipart wrappers) was always what
+ // showed up as the "body" rather than the actual message
+ // content. Still marks \Seen exactly like before (any non-.PEEK
+ // BODY fetch does).
+ std::string tag = nextTag();
+ sendLine(tag + " UID FETCH " + std::to_string(uid) + " (FLAGS BODY[])");
+ auto lines = readUntilTagged(tag);
+ if (!taggedOk(lines, tag))
+ throw coreutils::Exception("imap: FETCH failed", __FILE__, __LINE__, 502);
+
+ std::string rawMessage;
+ bool found = false;
+ for (auto &line : lines) {
+ if (line.rfind("* ", 0) != 0 || line.find("FETCH") == std::string::npos)
+ continue;
+ found = true;
+ rawMessage = findBodyLiteral(line);
+ }
+
+ if (!found)
+ throw coreutils::Exception("Message not found.", __FILE__, __LINE__, 404);
+
+ size_t headerEnd = rawMessage.find("\r\n\r\n");
+ std::string headerBlock = (headerEnd == std::string::npos) ? rawMessage : rawMessage.substr(0, headerEnd);
+ std::string bodyBlock = (headerEnd == std::string::npos) ? "" : rawMessage.substr(headerEnd + 4);
+
+ std::string from, to, subject, date;
+ parseHeaderFields(headerBlock, from, to, subject, date);
+ from = decodeMimeHeader(from);
+ to = decodeMimeHeader(to);
+ subject = decodeMimeHeader(subject);
+
+ MimeTextPart htmlPart, plainPart;
+ bool haveHtml = false, havePlain = false;
+ collectBestTextParts(headerBlock, bodyBlock, htmlPart, haveHtml, plainPart, havePlain);
+
+ std::string bodyText;
+ bool isHtml = false;
+ if (haveHtml) {
+ bodyText = htmlPart.decodedBody;
+ isHtml = true;
+ } else if (havePlain) {
+ bodyText = plainPart.decodedBody;
+ } else {
+ // Neither a text/html nor a text/plain part was found anywhere
+ // (an unusual message - e.g. one made up entirely of
+ // attachments) - fall back to the raw body rather than
+ // showing nothing at all.
+ bodyText = bodyBlock;
+ }
+
+ std::ostringstream json;
+ json << "{\"uid\":" << uid
+ << ",\"from\":\"" << jsonEscape(trimWS(from)) << "\""
+ << ",\"to\":\"" << jsonEscape(trimWS(to)) << "\""
+ << ",\"subject\":\"" << jsonEscape(trimWS(subject)) << "\""
+ << ",\"date\":\"" << jsonEscape(trimWS(date)) << "\""
+ << ",\"body\":\"" << jsonEscape(bodyText) << "\""
+ << ",\"isHtml\":" << (isHtml ? "true" : "false")
+ << "}";
+ return coreutils::MString(json.str());
+ }
+
+ coreutils::MString __imap::setFlag(const std::string &folder, uint32_t uid, const std::string &flag, bool add) {
+ if (!selectFolderIfNeeded(folder))
+ throw coreutils::Exception("imap: could not select folder " + folder, __FILE__, __LINE__, 502);
+ std::string tag = nextTag();
+ sendLine(tag + " UID STORE " + std::to_string(uid) + " " + (add ? "+FLAGS" : "-FLAGS") + " (" + flag + ")");
+ auto lines = readUntilTagged(tag);
+ bool ok = taggedOk(lines, tag);
+ return coreutils::MString(std::string("{\"ok\":") + (ok ? "true" : "false") + "}");
+ }
+
+ coreutils::MString __imap::deleteMessage(const std::string &folder, uint32_t uid) {
+ if (!selectFolderIfNeeded(folder))
+ throw coreutils::Exception("imap: could not select folder " + folder, __FILE__, __LINE__, 502);
+
+ std::string tag = nextTag();
+ sendLine(tag + " UID STORE " + std::to_string(uid) + " +FLAGS (\\Deleted)");
+ auto lines = readUntilTagged(tag);
+ bool ok1 = taggedOk(lines, tag);
+
+ std::string tag2 = nextTag();
+ sendLine(tag2 + " EXPUNGE");
+ auto lines2 = readUntilTagged(tag2);
+ bool ok2 = taggedOk(lines2, tag2);
+
+ bool ok = ok1 && ok2;
+ return coreutils::MString(std::string("{\"ok\":") + (ok ? "true" : "false") + "}");
+ }
+
+ // BARANTMail (see __IMAP_COPY.cpp) implements COPY but not the
+ // RFC 6851 MOVE extension - not surprising, since the whole point
+ // of that extension was standardizing what every real-world client
+ // already did by hand: COPY into the destination, then \Deleted +
+ // EXPUNGE from the source. Same two IMAP round trips deleteMessage()
+ // above already does, with a COPY in front.
+ coreutils::MString __imap::moveMessage(const std::string &folder, uint32_t uid, const std::string &destFolder) {
+ if (!selectFolderIfNeeded(folder))
+ throw coreutils::Exception("imap: could not select folder " + folder, __FILE__, __LINE__, 502);
+
+ std::string copyTag = nextTag();
+ sendLine(copyTag + " UID COPY " + std::to_string(uid) + " " + destFolder);
+ auto copyLines = readUntilTagged(copyTag);
+ if (!taggedOk(copyLines, copyTag))
+ return coreutils::MString("{\"ok\":false}");
+
+ std::string tag = nextTag();
+ sendLine(tag + " UID STORE " + std::to_string(uid) + " +FLAGS (\\Deleted)");
+ auto lines = readUntilTagged(tag);
+ bool ok1 = taggedOk(lines, tag);
+
+ std::string tag2 = nextTag();
+ sendLine(tag2 + " EXPUNGE");
+ auto lines2 = readUntilTagged(tag2);
+ bool ok2 = taggedOk(lines2, tag2);
+
+ bool ok = ok1 && ok2;
+ return coreutils::MString(std::string("{\"ok\":") + (ok ? "true" : "false") + "}");
+ }
+
+ // Message/unseen counts for the site's fixed 3-folder set (see
+ // email.menu - Inbox/Spam/Trash are the only folders this webmail
+ // client ever shows). Uses IMAP STATUS rather than a real FETCH,
+ // since it's just two integers per folder, not the full message
+ // data listMessages() pulls - BARANTMail's own __IMAP_STATUS.cpp
+ // doesn't require the folder to be unselected first, so this works
+ // regardless of whichever folder is currently SELECTed for other
+ // operations on this same pooled connection.
+ coreutils::MString __imap::folderCounts() {
+ static const char *folders[] = {"Inbox", "Spam", "Trash"};
+ std::ostringstream json;
+ json << "{";
+ bool first = true;
+ for (const char *folder : folders) {
+ std::string tag = nextTag();
+ sendLine(tag + " STATUS " + folder + " (MESSAGES UNSEEN)");
+ auto lines = readUntilTagged(tag);
+
+ int messages = 0, unseen = 0;
+ if (taggedOk(lines, tag)) {
+ for (auto &line : lines) {
+ if (line.rfind("* STATUS", 0) != 0)
+ continue;
+ size_t mPos = line.find("MESSAGES ");
+ if (mPos != std::string::npos)
+ messages = atoi(line.c_str() + mPos + 9);
+ size_t uPos = line.find("UNSEEN ");
+ if (uPos != std::string::npos)
+ unseen = atoi(line.c_str() + uPos + 7);
+ }
+ }
+
+ if (!first)
+ json << ",";
+ first = false;
+ json << "\"" << folder << "\":{\"messages\":" << messages << ",\"unseen\":" << unseen << "}";
+ }
+ json << "}";
+ return coreutils::MString(json.str());
+ }
+
+}
diff --git a/__imap.h b/__imap.h
new file mode 100644
index 0000000..b04d44b
--- /dev/null
+++ b/__imap.h
@@ -0,0 +1,112 @@
+#ifndef ____imap_h__
+# define ____imap_h__
+
+# include "Tag.h"
+# include "ZString.h"
+# include "MString.h"
+# include
+# include
+# include
+# include
+
+namespace jet {
+
+ ///
+ ///
+ ///
+ /// Connects to the IMAP host named in the JSON file `config` (keys:
+ /// host, port, caCertificate - see email.cfg), TLS-handshakes
+ /// (implicit TLS, matching IMAP's standard port 993 - no STARTTLS
+ /// negotiation), and LOGINs with user/password. Throws
+ /// coreutils::Exception (with errorNumber set to a real HTTP status -
+ /// 401 for a bad login, 502 for a connection/protocol failure) on any
+ /// failure, exactly like __mysql throws on a failed
+ /// mysql_real_connect() - see ApiSession.cpp's exception->HTTP-status
+ /// mapping, which both this tag and __mysql rely on identically.
+ ///
+ /// Registers itself into Global::imapSessions under sessionid (a
+ /// SEPARATE map from __mysql's Global::sessions - the two backends
+ /// aren't interchangeable and Global::sessions is concretely typed
+ /// __mysql*, not something a second backend could share without
+ /// widening every existing __mysql-only call site), the same way
+ /// __mysql registers into Global::sessions - later __imap* -family
+ /// tags (__imaplist, __imapmessage, __imapflag, __imapdelete) look
+ /// the live connection up by sessionid, mirroring how finds its
+ /// by sessionid.
+ ///
+ /// Connection pooling: a thread_local pool keyed by sessionid+user,
+ /// reused across requests on the same APIServer worker thread (an
+ /// IMAP TCP+TLS+LOGIN round trip is too expensive to redo on every
+ /// AJAX call the webmail UI makes) - directly mirrors __mysql.cpp's
+ /// own thread_local connectionPool, including using a cheap
+ /// keepalive probe (NOOP, IMAP's equivalent of mysql_ping()) to
+ /// detect and discard a connection the server itself already dropped
+ /// rather than handing a dead socket to the first real command.
+ ///
+
+ class __imap : public Tag {
+
+ public:
+ __imap(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
+ ~__imap();
+
+ // Each returns a complete, ready-to-emit JSON value (object or
+ // array) - the __imap*-family wrapper tags (__imaplist.cpp etc.)
+ // just write whatever this returns straight into their own `out`,
+ // the same one-call-does-everything shape __sql's query() call
+ // has relative to .
+ coreutils::MString listMessages(const std::string &folder, int limit);
+ coreutils::MString searchMessages(const std::string &folder, const std::string &query);
+ coreutils::MString fetchMessage(const std::string &folder, uint32_t uid);
+ coreutils::MString setFlag(const std::string &folder, uint32_t uid, const std::string &flag, bool add);
+ coreutils::MString deleteMessage(const std::string &folder, uint32_t uid);
+ coreutils::MString moveMessage(const std::string &folder, uint32_t uid, const std::string &destFolder);
+ coreutils::MString folderCounts();
+
+ coreutils::MString sessionId;
+
+ private:
+ SSL_CTX *ctx = nullptr;
+ SSL *ssl = nullptr;
+ int fd = -1;
+ int tagCounter = 0;
+ std::string recvBuffer;
+ size_t recvPos = 0;
+ std::string selectedFolder;
+
+ std::string nextTag();
+ void sendLine(const std::string &line);
+ char readByte();
+ // Reads one IMAP response line, INCLUDING any literal {n} content
+ // that line announces (rejoined into the same returned string,
+ // separated by "\r\n" markers) - see the .cpp for the full
+ // rationale; this is a pragmatic, purpose-built reader for the
+ // specific, known response shapes this client's own requests
+ // provoke from BARANTMail (whose own FETCH implementation - built
+ // earlier this same session - always uses literals for content,
+ // never quoted strings), not a fully general IMAP parser.
+ std::string readResponseLine();
+ // Reads response lines until the tagged completion line for `tag`
+ // (built earlier via nextTag()) appears; returns every line
+ // (untagged and the final tagged one) collected along the way.
+ // Throws if the tagged line's status isn't OK.
+ std::vector readUntilTagged(const std::string &tag);
+
+ bool selectFolderIfNeeded(const std::string &folder);
+ static std::string jsonEscape(const std::string &s);
+ static uint32_t findUidField(const std::string &fetchLine);
+ static std::vector findFlagsField(const std::string &fetchLine);
+ static long findSizeField(const std::string &fetchLine);
+ // Extracts the literal content immediately following a
+ // "BODY[...]" (or "BODY[...]") marker in an already-
+ // literal-rejoined response line - see readResponseLine()'s own
+ // doc comment for the "\r\n"-separated rejoining this depends on.
+ static std::string findBodyLiteral(const std::string &fetchLine);
+ // Shared by listMessages() and searchMessages() - see its own
+ // doc comment in the .cpp.
+ static std::vector parseSummaryLines(const std::vector &lines);
+ };
+
+}
+
+#endif
diff --git a/__imapaccounts.cpp b/__imapaccounts.cpp
new file mode 100644
index 0000000..cbb16e6
--- /dev/null
+++ b/__imapaccounts.cpp
@@ -0,0 +1,139 @@
+#include "__imapaccounts.h"
+#include "Base64.h"
+#include "Exception.h"
+
+#include
+
+namespace jet {
+
+ std::string __imapaccounts::jsonEscape(const std::string &s) {
+ std::string out;
+ out.reserve(s.size() + 8);
+ for (unsigned char c : s) {
+ switch (c) {
+ case '"': out += "\\\""; break;
+ case '\\': out += "\\\\"; break;
+ case '\n': out += "\\n"; break;
+ case '\r': continue;
+ case '\t': out += "\\t"; break;
+ default:
+ if (c < 0x20) {
+ char buf[8];
+ snprintf(buf, sizeof(buf), "\\u%04x", c);
+ out += buf;
+ } else {
+ out += (char)c;
+ }
+ }
+ }
+ return out;
+ }
+
+ static std::string trimWhitespace(const std::string &s) {
+ size_t start = s.find_first_not_of(" \t\r\n");
+ if (start == std::string::npos)
+ return std::string();
+ size_t end = s.find_last_not_of(" \t\r\n");
+ return s.substr(start, end - start + 1);
+ }
+
+ std::vector> __imapaccounts::decodeList(const std::string &encodedRaw) {
+ std::vector> entries;
+ coreutils::Base64 base64;
+ // The list value round-trips through / template
+ // captures, which include whatever whitespace/newlines surround
+ // the tag call in the .jet source - trimming here (rather than
+ // insisting every template call site stays whitespace-free) is
+ // the robust fix, since a base64 decoder that stops at the first
+ // invalid character would otherwise silently truncate to nothing
+ // when a stray leading newline precedes real, valid content.
+ std::string encoded = trimWhitespace(encodedRaw);
+ size_t pos = 0;
+ while (pos < encoded.size()) {
+ size_t semi = encoded.find(';', pos);
+ std::string entry = (semi == std::string::npos) ? encoded.substr(pos) : encoded.substr(pos, semi - pos);
+ pos = (semi == std::string::npos) ? encoded.size() : semi + 1;
+ entry = trimWhitespace(entry);
+ if (entry.empty())
+ continue;
+ size_t colon = entry.find(':');
+ if (colon == std::string::npos)
+ continue;
+ std::string userB64 = trimWhitespace(entry.substr(0, colon));
+ std::string passB64 = trimWhitespace(entry.substr(colon + 1));
+ std::string user = base64.decode(userB64).str();
+ std::string password = base64.decode(passB64).str();
+ entries.push_back(std::make_pair(user, password));
+ }
+ return entries;
+ }
+
+ std::string __imapaccounts::encodeList(const std::vector> &entries) {
+ coreutils::Base64 base64;
+ std::string result;
+ for (size_t i = 0; i < entries.size(); i++) {
+ if (i > 0)
+ result += ";";
+ std::string user = entries[i].first;
+ std::string password = entries[i].second;
+ result += base64.encode(user).str();
+ result += ":";
+ result += base64.encode(password).str();
+ }
+ return result;
+ }
+
+ __imapaccounts::__imapaccounts(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
+
+ std::string action = trimWhitespace(resolveKeyword("action").str());
+ std::string listStr = resolveKeyword("list").str();
+ auto entries = decodeList(listStr);
+
+ if (action == "add") {
+ std::string user = trimWhitespace(resolveKeyword("user").str());
+ std::string password = trimWhitespace(resolveKeyword("password").str());
+ for (size_t i = 0; i < entries.size(); i++) {
+ if (entries[i].first == user) {
+ entries.erase(entries.begin() + (long)i);
+ break;
+ }
+ }
+ entries.push_back(std::make_pair(user, password));
+ out = coreutils::MString(encodeList(entries));
+ } else if (action == "remove") {
+ std::string user = trimWhitespace(resolveKeyword("user").str());
+ for (size_t i = 0; i < entries.size(); i++) {
+ if (entries[i].first == user) {
+ entries.erase(entries.begin() + (long)i);
+ break;
+ }
+ }
+ out = coreutils::MString(encodeList(entries));
+ } else if (action == "getpassword") {
+ std::string user = trimWhitespace(resolveKeyword("user").str());
+ std::string password;
+ for (size_t i = 0; i < entries.size(); i++) {
+ if (entries[i].first == user) {
+ password = entries[i].second;
+ break;
+ }
+ }
+ out = coreutils::MString(password);
+ } else if (action == "firstuser") {
+ out = coreutils::MString(entries.empty() ? std::string() : entries[0].first);
+ } else if (action == "list") {
+ std::string active = resolveKeyword("active").str();
+ std::string json = "{\"accounts\":[";
+ for (size_t i = 0; i < entries.size(); i++) {
+ if (i > 0)
+ json += ",";
+ json += "\"" + jsonEscape(entries[i].first) + "\"";
+ }
+ json += "],\"active\":\"" + jsonEscape(active) + "\"}";
+ out = coreutils::MString(json);
+ } else {
+ throw coreutils::Exception("imapaccounts: unknown action \"" + action + "\"", __FILE__, __LINE__, 500);
+ }
+ }
+
+}
diff --git a/__imapaccounts.h b/__imapaccounts.h
new file mode 100644
index 0000000..1261a87
--- /dev/null
+++ b/__imapaccounts.h
@@ -0,0 +1,59 @@
+#ifndef ____imapaccounts_h__
+# define ____imapaccounts_h__
+
+# include "Tag.h"
+# include "ZString.h"
+# include "MString.h"
+# include
+# include
+# include
+
+namespace jet {
+
+ ///
+ ///
+ ///
+ /// Pure list-transform tag (no session-variable access of its own,
+ /// no IMAP connection) that manages the multi-account list webmail
+ /// keeps in the single session variable "_imap_accounts" - the .jet
+ /// route wraps each call in itself and
+ /// s it back, the same separation-of-concerns every other
+ /// session variable in this codebase already uses (this tag never
+ /// touches Global's session-variable map directly).
+ ///
+ /// The list's own on-the-wire encoding (semicolon-separated
+ /// "base64(user):base64(password)" entries) is private to this tag -
+ /// callers only ever pass the whole `list` value back in verbatim
+ /// and use the returned value the same way, so the choice of
+ /// delimiter can't collide with any real username or password
+ /// character.
+ ///
+ /// action="add": upserts user/password (removing any existing entry
+ /// for the same user first, so reconnecting with a changed password
+ /// updates it) - returns the updated encoded list.
+ /// action="remove": drops the entry for user, if present - returns
+ /// the updated encoded list.
+ /// action="getpassword": returns the stored password for user, or
+ /// an empty string if not present.
+ /// action="firstuser": returns the first entry's username, or an
+ /// empty string if the list is empty.
+ /// action="list": returns JSON ({"accounts":[...],"active":"..."})
+ /// - the one client-facing action; "active" echoes the `active`
+ /// keyword (typically $[@_imap_user]) back verbatim so the sidebar
+ /// can highlight the current one, and never includes any password.
+ ///
+ class __imapaccounts : public Tag {
+
+ public:
+ __imapaccounts(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
+
+ private:
+ static std::vector> decodeList(const std::string &encoded);
+ static std::string encodeList(const std::vector> &entries);
+ static std::string jsonEscape(const std::string &s);
+ };
+
+}
+
+#endif
diff --git a/__imapdelete.cpp b/__imapdelete.cpp
new file mode 100644
index 0000000..e8ab3a7
--- /dev/null
+++ b/__imapdelete.cpp
@@ -0,0 +1,25 @@
+#include "__imapdelete.h"
+#include "Exception.h"
+#include "__imap.h"
+#include
+
+namespace jet {
+
+ __imapdelete::__imapdelete(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
+
+ coreutils::MString sessionId = resolveKeyword("sessionid");
+ if (!global.imapSessionExists(sessionId))
+ throw coreutils::Exception("imap sessionid does not exist.", __FILE__, __LINE__, 502);
+
+ std::string folder = resolveKeyword("folder").str();
+ if (folder.empty())
+ folder = "INBOX";
+
+ if (!keywordDefined("uid"))
+ throw coreutils::Exception("imapdelete tag must have uid defined.", __FILE__, __LINE__, 400);
+ uint32_t uid = (uint32_t)strtoul(resolveKeyword("uid").str().c_str(), nullptr, 10);
+
+ out = global.getImapSession(sessionId)->deleteMessage(folder, uid);
+ }
+
+}
diff --git a/__imapdelete.h b/__imapdelete.h
new file mode 100644
index 0000000..2897e77
--- /dev/null
+++ b/__imapdelete.h
@@ -0,0 +1,25 @@
+#ifndef ____imapdelete_h__
+# define ____imapdelete_h__
+
+# include "Tag.h"
+
+namespace jet {
+
+ ///
+ ///
+ ///
+ /// Self-closing tag - marks \Deleted and EXPUNGEs one message,
+ /// writes {"ok":true|false} into `out`. See __imaplist.h's doc
+ /// comment for how `out` reaches the real response body.
+ ///
+
+ class __imapdelete : public Tag {
+
+ public:
+ __imapdelete(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
+
+ };
+
+}
+
+#endif
diff --git a/__imapflag.cpp b/__imapflag.cpp
new file mode 100644
index 0000000..ab3a247
--- /dev/null
+++ b/__imapflag.cpp
@@ -0,0 +1,32 @@
+#include "__imapflag.h"
+#include "Exception.h"
+#include "__imap.h"
+#include
+
+namespace jet {
+
+ __imapflag::__imapflag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
+
+ coreutils::MString sessionId = resolveKeyword("sessionid");
+ if (!global.imapSessionExists(sessionId))
+ throw coreutils::Exception("imap sessionid does not exist.", __FILE__, __LINE__, 502);
+
+ std::string folder = resolveKeyword("folder").str();
+ if (folder.empty())
+ folder = "INBOX";
+
+ if (!keywordDefined("uid"))
+ throw coreutils::Exception("imapflag tag must have uid defined.", __FILE__, __LINE__, 400);
+ uint32_t uid = (uint32_t)strtoul(resolveKeyword("uid").str().c_str(), nullptr, 10);
+
+ if (!keywordDefined("flag"))
+ throw coreutils::Exception("imapflag tag must have flag defined.", __FILE__, __LINE__, 400);
+ std::string flag = resolveKeyword("flag").str();
+
+ std::string setStr = resolveKeyword("set").str();
+ bool add = setStr.empty() || setStr == "yes" || setStr == "true";
+
+ out = global.getImapSession(sessionId)->setFlag(folder, uid, flag, add);
+ }
+
+}
diff --git a/__imapflag.h b/__imapflag.h
new file mode 100644
index 0000000..37febd2
--- /dev/null
+++ b/__imapflag.h
@@ -0,0 +1,26 @@
+#ifndef ____imapflag_h__
+# define ____imapflag_h__
+
+# include "Tag.h"
+
+namespace jet {
+
+ ///
+ ///
+ ///
+ /// Self-closing tag - STOREs (adds, or removes when set="no") a
+ /// single IMAP flag on one message, writes {"ok":true|false} into
+ /// `out`. See __imaplist.h's doc comment for how `out` reaches the
+ /// real response body.
+ ///
+
+ class __imapflag : public Tag {
+
+ public:
+ __imapflag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
+
+ };
+
+}
+
+#endif
diff --git a/__imapfolders.cpp b/__imapfolders.cpp
new file mode 100644
index 0000000..86106e7
--- /dev/null
+++ b/__imapfolders.cpp
@@ -0,0 +1,16 @@
+#include "__imapfolders.h"
+#include "Exception.h"
+#include "__imap.h"
+
+namespace jet {
+
+ __imapfolders::__imapfolders(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
+
+ coreutils::MString sessionId = resolveKeyword("sessionid");
+ if (!global.imapSessionExists(sessionId))
+ throw coreutils::Exception("imap sessionid does not exist.", __FILE__, __LINE__, 502);
+
+ out = global.getImapSession(sessionId)->folderCounts();
+ }
+
+}
diff --git a/__imapfolders.h b/__imapfolders.h
new file mode 100644
index 0000000..21c1395
--- /dev/null
+++ b/__imapfolders.h
@@ -0,0 +1,27 @@
+#ifndef ____imapfolders_h__
+# define ____imapfolders_h__
+
+# include "Tag.h"
+
+namespace jet {
+
+ ///
+ ///
+ ///
+ /// Self-closing tag - writes {"Inbox":{"messages":n,"unseen":n},
+ /// "Spam":{...},"Trash":{...}} into `out` (see
+ /// __imap::folderCounts()'s own doc comment for why it's this fixed
+ /// 3-folder set specifically). See __imaplist.h's doc comment for
+ /// how `out` reaches the real response body.
+ ///
+
+ class __imapfolders : public Tag {
+
+ public:
+ __imapfolders(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
+
+ };
+
+}
+
+#endif
diff --git a/__imaplist.cpp b/__imaplist.cpp
new file mode 100644
index 0000000..46acb69
--- /dev/null
+++ b/__imaplist.cpp
@@ -0,0 +1,26 @@
+#include "__imaplist.h"
+#include "Exception.h"
+#include "__imap.h"
+#include
+
+namespace jet {
+
+ __imaplist::__imaplist(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
+
+ coreutils::MString sessionId = resolveKeyword("sessionid");
+ if (!global.imapSessionExists(sessionId))
+ throw coreutils::Exception("imap sessionid does not exist.", __FILE__, __LINE__, 502);
+
+ std::string folder = resolveKeyword("folder").str();
+ if (folder.empty())
+ folder = "INBOX";
+
+ std::string limitStr = resolveKeyword("limit").str();
+ int limit = limitStr.empty() ? 50 : atoi(limitStr.c_str());
+ if (limit <= 0)
+ limit = 50;
+
+ out = global.getImapSession(sessionId)->listMessages(folder, limit);
+ }
+
+}
diff --git a/__imaplist.h b/__imaplist.h
new file mode 100644
index 0000000..a59a9a2
--- /dev/null
+++ b/__imaplist.h
@@ -0,0 +1,30 @@
+#ifndef ____imaplist_h__
+# define ____imaplist_h__
+
+# include "Tag.h"
+
+namespace jet {
+
+ ///
+ ///
+ ///
+ /// Self-closing tag (no container) - writes a JSON array of message
+ /// summaries for `folder`, newest first, straight into `out`, the
+ /// same way any other self-closing value tag with default
+ /// output=true/evaluate=true does: Tag::~Tag() copies `out` into the
+ /// caller's parentOut verbatim (Tag::copyContainer() is a raw byte
+ /// copy, no re-evaluation - see Tag.cpp - so literal "$"/"["/"<" in
+ /// real email subjects pass through unmolested; __imap::jsonEscape()
+ /// already handled quote/backslash/control-char JSON-escaping).
+ ///
+
+ class __imaplist : public Tag {
+
+ public:
+ __imaplist(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
+
+ };
+
+}
+
+#endif
diff --git a/__imapmessage.cpp b/__imapmessage.cpp
new file mode 100644
index 0000000..6a2c66c
--- /dev/null
+++ b/__imapmessage.cpp
@@ -0,0 +1,25 @@
+#include "__imapmessage.h"
+#include "Exception.h"
+#include "__imap.h"
+#include
+
+namespace jet {
+
+ __imapmessage::__imapmessage(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
+
+ coreutils::MString sessionId = resolveKeyword("sessionid");
+ if (!global.imapSessionExists(sessionId))
+ throw coreutils::Exception("imap sessionid does not exist.", __FILE__, __LINE__, 502);
+
+ std::string folder = resolveKeyword("folder").str();
+ if (folder.empty())
+ folder = "INBOX";
+
+ if (!keywordDefined("uid"))
+ throw coreutils::Exception("imapmessage tag must have uid defined.", __FILE__, __LINE__, 400);
+ uint32_t uid = (uint32_t)strtoul(resolveKeyword("uid").str().c_str(), nullptr, 10);
+
+ out = global.getImapSession(sessionId)->fetchMessage(folder, uid);
+ }
+
+}
diff --git a/__imapmessage.h b/__imapmessage.h
new file mode 100644
index 0000000..485ba94
--- /dev/null
+++ b/__imapmessage.h
@@ -0,0 +1,25 @@
+#ifndef ____imapmessage_h__
+# define ____imapmessage_h__
+
+# include "Tag.h"
+
+namespace jet {
+
+ ///
+ ///
+ ///
+ /// Self-closing tag - writes a single JSON message object (headers +
+ /// text body) into `out`. See __imaplist.h's doc comment for how
+ /// `out` reaches the real response body.
+ ///
+
+ class __imapmessage : public Tag {
+
+ public:
+ __imapmessage(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
+
+ };
+
+}
+
+#endif
diff --git a/__imapmove.cpp b/__imapmove.cpp
new file mode 100644
index 0000000..c2ce20e
--- /dev/null
+++ b/__imapmove.cpp
@@ -0,0 +1,29 @@
+#include "__imapmove.h"
+#include "Exception.h"
+#include "__imap.h"
+#include
+
+namespace jet {
+
+ __imapmove::__imapmove(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
+
+ coreutils::MString sessionId = resolveKeyword("sessionid");
+ if (!global.imapSessionExists(sessionId))
+ throw coreutils::Exception("imap sessionid does not exist.", __FILE__, __LINE__, 502);
+
+ std::string folder = resolveKeyword("folder").str();
+ if (folder.empty())
+ folder = "INBOX";
+
+ if (!keywordDefined("uid"))
+ throw coreutils::Exception("imapmove tag must have uid defined.", __FILE__, __LINE__, 400);
+ uint32_t uid = (uint32_t)strtoul(resolveKeyword("uid").str().c_str(), nullptr, 10);
+
+ if (!keywordDefined("destfolder"))
+ throw coreutils::Exception("imapmove tag must have destfolder defined.", __FILE__, __LINE__, 400);
+ std::string destFolder = resolveKeyword("destfolder").str();
+
+ out = global.getImapSession(sessionId)->moveMessage(folder, uid, destFolder);
+ }
+
+}
diff --git a/__imapmove.h b/__imapmove.h
new file mode 100644
index 0000000..7051ede
--- /dev/null
+++ b/__imapmove.h
@@ -0,0 +1,29 @@
+#ifndef ____imapmove_h__
+# define ____imapmove_h__
+
+# include "Tag.h"
+
+namespace jet {
+
+ ///
+ ///
+ ///
+ /// Self-closing tag - moves one message into destfolder (COPY there,
+ /// then \Deleted + EXPUNGE from the original folder, the standard
+ /// pre-RFC6851 two-step "move" every IMAP client used before the
+ /// MOVE extension existed - see __imap::moveMessage()'s own doc
+ /// comment for why this server doesn't just speak that extension
+ /// directly). Writes {"ok":true|false} into `out`, same convention
+ /// as __imapdelete/__imapflag.
+ ///
+
+ class __imapmove : public Tag {
+
+ public:
+ __imapmove(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
+
+ };
+
+}
+
+#endif
diff --git a/__imapsearch.cpp b/__imapsearch.cpp
new file mode 100644
index 0000000..877a0c4
--- /dev/null
+++ b/__imapsearch.cpp
@@ -0,0 +1,22 @@
+#include "__imapsearch.h"
+#include "Exception.h"
+#include "__imap.h"
+
+namespace jet {
+
+ __imapsearch::__imapsearch(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
+
+ coreutils::MString sessionId = resolveKeyword("sessionid");
+ if (!global.imapSessionExists(sessionId))
+ throw coreutils::Exception("imap sessionid does not exist.", __FILE__, __LINE__, 502);
+
+ std::string folder = resolveKeyword("folder").str();
+ if (folder.empty())
+ folder = "INBOX";
+
+ std::string query = resolveKeyword("query").str();
+
+ out = global.getImapSession(sessionId)->searchMessages(folder, query);
+ }
+
+}
diff --git a/__imapsearch.h b/__imapsearch.h
new file mode 100644
index 0000000..aac4120
--- /dev/null
+++ b/__imapsearch.h
@@ -0,0 +1,27 @@
+#ifndef ____imapsearch_h__
+# define ____imapsearch_h__
+
+# include "Tag.h"
+
+namespace jet {
+
+ ///
+ ///
+ ///
+ /// Self-closing tag - writes a JSON array of message summaries
+ /// (same shape as ) for every message in `folder` whose
+ /// From, Subject, or body matches `query` (see
+ /// __imap::searchMessages()'s own doc comment), newest first,
+ /// straight into `out`.
+ ///
+
+ class __imapsearch : public Tag {
+
+ public:
+ __imapsearch(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
+
+ };
+
+}
+
+#endif
diff --git a/__smtpsend.cpp b/__smtpsend.cpp
new file mode 100644
index 0000000..de96709
--- /dev/null
+++ b/__smtpsend.cpp
@@ -0,0 +1,220 @@
+#include "__smtpsend.h"
+#include "Exception.h"
+#include "JFile.h"
+#include "Base64.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace jet {
+
+ void __smtpsend::sendLine(const std::string &line) {
+ std::string data = line + "\r\n";
+ size_t sent = 0;
+ while (sent < data.size()) {
+ int n = SSL_write(ssl, data.data() + sent, (int)(data.size() - sent));
+ if (n <= 0)
+ throw coreutils::Exception("smtpsend: write failed", __FILE__, __LINE__, 502);
+ sent += (size_t)n;
+ }
+ }
+
+ char __smtpsend::readByte() {
+ if (recvPos >= recvBuffer.size()) {
+ char buf[4096];
+ int n = SSL_read(ssl, buf, sizeof(buf));
+ if (n <= 0)
+ throw coreutils::Exception("smtpsend: read failed", __FILE__, __LINE__, 502);
+ recvBuffer.assign(buf, buf + n);
+ recvPos = 0;
+ }
+ return recvBuffer[recvPos++];
+ }
+
+ std::string __smtpsend::readLine() {
+ std::string line;
+ char c;
+ for (;;) {
+ c = readByte();
+ if (c == '\n')
+ break;
+ if (c != '\r')
+ line += c;
+ }
+ return line;
+ }
+
+ int __smtpsend::readReply(int expectedCode) {
+ int code = 0;
+ std::string line;
+ for (;;) {
+ line = readLine();
+ if (line.size() < 4)
+ throw coreutils::Exception("smtpsend: malformed reply: " + line, __FILE__, __LINE__, 502);
+ code = atoi(line.substr(0, 3).c_str());
+ if (line[3] == ' ')
+ break;
+ // else line[3] == '-': a continuation line, same code - keep reading.
+ }
+ if (code / 100 != expectedCode / 100)
+ throw coreutils::Exception("smtpsend: unexpected reply " + std::to_string(code) + ": " + line, __FILE__, __LINE__, 502);
+ return code;
+ }
+
+ void __smtpsend::closeConnection() {
+ if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); ssl = nullptr; }
+ if (fd >= 0) { ::close(fd); fd = -1; }
+ if (ctx) { SSL_CTX_free(ctx); ctx = nullptr; }
+ }
+
+ std::string __smtpsend::base64Encode(const std::string &s) {
+ coreutils::Base64 base64;
+ std::string copy = s;
+ coreutils::ZString z(copy);
+ return base64.encode(z).str();
+ }
+
+ std::string __smtpsend::rfc2822Date() {
+ time_t now = time(nullptr);
+ struct tm tmResult{};
+ gmtime_r(&now, &tmResult);
+ char buf[64];
+ strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S +0000", &tmResult);
+ return std::string(buf);
+ }
+
+ std::string __smtpsend::buildMessage(const std::string &from, const std::string &to,
+ const std::string &subject, const std::string &html) {
+ std::string msg;
+ msg += "Date: " + rfc2822Date() + "\r\n";
+ msg += "From: " + from + "\r\n";
+ msg += "To: " + to + "\r\n";
+ msg += "Subject: " + subject + "\r\n";
+ msg += "MIME-Version: 1.0\r\n";
+ msg += "Content-Type: text/html; charset=UTF-8\r\n";
+ msg += "Content-Transfer-Encoding: 8bit\r\n";
+ msg += "\r\n";
+ // SMTP DATA's dot-stuffing rule: any body line starting with '.'
+ // needs a second '.' prepended, or the server treats it as the
+ // end-of-data marker prematurely and truncates the message.
+ std::string stuffed;
+ stuffed.reserve(html.size());
+ bool atLineStart = true;
+ for (size_t i = 0; i < html.size(); i++) {
+ char c = html[i];
+ if (atLineStart && c == '.')
+ stuffed += '.';
+ stuffed += c;
+ atLineStart = (c == '\n');
+ }
+ msg += stuffed;
+ return msg;
+ }
+
+ __smtpsend::__smtpsend(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
+
+ std::string user = resolveKeyword("user").str();
+ std::string password = resolveKeyword("password").str();
+ std::string to = resolveKeyword("to").str();
+ std::string subject = resolveKeyword("subject").str();
+ std::string html = resolveKeyword("html").str();
+ coreutils::MString configPath = resolveKeyword("config");
+
+ if (user.empty() || password.empty())
+ throw coreutils::Exception("smtpsend: user/password must be specified.", __FILE__, __LINE__, 401);
+ if (to.empty())
+ throw coreutils::Exception("smtpsend: to address must be specified.", __FILE__, __LINE__, 400);
+
+ coreutils::JFile config(configPath);
+ std::string host = config.getString("host");
+ long port = (long)config.getNumber("smtpPort");
+ if (port <= 0)
+ port = 465;
+
+ struct addrinfo hints {};
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ struct addrinfo *result = nullptr;
+ std::string portStr = std::to_string(port);
+ if (getaddrinfo(host.c_str(), portStr.c_str(), &hints, &result) != 0 || !result)
+ throw coreutils::Exception("smtpsend: could not resolve " + host, __FILE__, __LINE__, 502);
+
+ int newFd = -1;
+ for (struct addrinfo *rp = result; rp != nullptr; rp = rp->ai_next) {
+ newFd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ if (newFd < 0)
+ continue;
+ if (::connect(newFd, rp->ai_addr, rp->ai_addrlen) == 0)
+ break;
+ ::close(newFd);
+ newFd = -1;
+ }
+ freeaddrinfo(result);
+ if (newFd < 0)
+ throw coreutils::Exception("smtpsend: could not connect to " + host, __FILE__, __LINE__, 502);
+
+ struct timeval timeout { 30, 0 };
+ setsockopt(newFd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
+ setsockopt(newFd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
+
+ SSL_CTX *newCtx = SSL_CTX_new(SSLv23_client_method());
+ if (!newCtx) {
+ ::close(newFd);
+ throw coreutils::Exception("smtpsend: SSL_CTX_new failed", __FILE__, __LINE__, 502);
+ }
+ SSL *newSsl = SSL_new(newCtx);
+ SSL_set_fd(newSsl, newFd);
+ if (SSL_connect(newSsl) != 1) {
+ SSL_free(newSsl);
+ SSL_CTX_free(newCtx);
+ ::close(newFd);
+ throw coreutils::Exception("smtpsend: TLS handshake failed", __FILE__, __LINE__, 502);
+ }
+
+ ctx = newCtx;
+ ssl = newSsl;
+ fd = newFd;
+
+ try {
+ readReply(220);
+
+ sendLine("EHLO " + host);
+ readReply(250);
+
+ sendLine("AUTH LOGIN");
+ readReply(334);
+ sendLine(base64Encode(user));
+ readReply(334);
+ sendLine(base64Encode(password));
+ readReply(235);
+
+ sendLine("MAIL FROM:<" + user + ">");
+ readReply(250);
+ sendLine("RCPT TO:<" + to + ">");
+ readReply(250);
+ sendLine("DATA");
+ readReply(354);
+
+ std::string message = buildMessage(user, to, subject, html);
+ sendLine(message);
+ sendLine(".");
+ readReply(250);
+
+ sendLine("QUIT");
+ readReply(221);
+ } catch (...) {
+ closeConnection();
+ throw;
+ }
+
+ closeConnection();
+
+ out = coreutils::MString(std::string("{\"ok\":true}"));
+ }
+
+}
diff --git a/__smtpsend.h b/__smtpsend.h
new file mode 100644
index 0000000..6438a22
--- /dev/null
+++ b/__smtpsend.h
@@ -0,0 +1,69 @@
+#ifndef ____smtpsend_h__
+# define ____smtpsend_h__
+
+# include "Tag.h"
+# include "ZString.h"
+# include "MString.h"
+# include
+# include
+
+namespace jet {
+
+ ///
+ ///
+ ///
+ /// One-shot authenticated SMTP submission to BARANTMAIL's own
+ /// implicit-TLS submission port (config key "smtpPort" - matches
+ /// barantmail.cfg's smtpTLS.port, :465, the same implicit-TLS style
+ /// as IMAP's :993 - no STARTTLS negotiation). Connects, TLS-
+ /// handshakes, AUTH LOGIN with the SAME mailbox credentials already
+ /// used by (so the message goes out authenticated as the
+ /// actual connected mailbox, not the site's own transactional-mail
+ /// identity - see barant.tag's separate tag for that), then
+ /// MAIL FROM/RCPT TO/DATAs a freshly-built RFC 5322 message
+ /// (Date/From/To/Subject/MIME headers, HTML body), then QUITs. The
+ /// authenticated user IS the From address - SMTP submission
+ /// requires MAIL FROM to match the authenticated identity anyway, so
+ /// there's no separate "from" keyword.
+ ///
+ /// No connection pooling, unlike __imap - composing mail is rare
+ /// enough that a fresh connect/auth/send/close per message is fine.
+ ///
+ /// Throws coreutils::Exception (401 for a bad login, 502 for any
+ /// other connect/protocol/rejected-recipient failure) on any
+ /// failure - same convention as __imap.
+ ///
+ class __smtpsend : public Tag {
+
+ public:
+ __smtpsend(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local);
+
+ private:
+ SSL_CTX *ctx = nullptr;
+ SSL *ssl = nullptr;
+ int fd = -1;
+ std::string recvBuffer;
+ size_t recvPos = 0;
+
+ void sendLine(const std::string &line);
+ char readByte();
+ std::string readLine();
+ // Reads one (possibly multi-line, "250-text"/"250 text"
+ // continuation-marked) SMTP reply and returns its 3-digit code;
+ // throws if the code's first digit doesn't match expectedCode's.
+ int readReply(int expectedCode);
+
+ void closeConnection();
+
+ static std::string base64Encode(const std::string &s);
+ static std::string rfc2822Date();
+ // Builds the full RFC 5322 message (headers + dot-stuffed body)
+ // handed to SMTP's DATA command.
+ static std::string buildMessage(const std::string &from, const std::string &to,
+ const std::string &subject, const std::string &html);
+ };
+
+}
+
+#endif
diff --git a/jetc/barant-site/assets/images/AvenirLTStd-Light.ttf b/jetc/barant-site/assets/images/AvenirLTStd-Light.ttf
new file mode 100644
index 0000000..8e717ce
Binary files /dev/null and b/jetc/barant-site/assets/images/AvenirLTStd-Light.ttf differ
diff --git a/jetc/barant-site/assets/images/Exo2-RegularCondensed.otf b/jetc/barant-site/assets/images/Exo2-RegularCondensed.otf
new file mode 100755
index 0000000..491a432
Binary files /dev/null and b/jetc/barant-site/assets/images/Exo2-RegularCondensed.otf differ
diff --git a/jetc/barant-site/assets/images/ServerCoreMap.png b/jetc/barant-site/assets/images/ServerCoreMap.png
new file mode 100644
index 0000000..35cbeda
Binary files /dev/null and b/jetc/barant-site/assets/images/ServerCoreMap.png differ
diff --git a/jetc/barant-site/assets/images/arant2.jpg b/jetc/barant-site/assets/images/arant2.jpg
new file mode 100644
index 0000000..c4434bb
Binary files /dev/null and b/jetc/barant-site/assets/images/arant2.jpg differ
diff --git a/jetc/barant-site/assets/images/back.png b/jetc/barant-site/assets/images/back.png
new file mode 100644
index 0000000..cfc858c
Binary files /dev/null and b/jetc/barant-site/assets/images/back.png differ
diff --git a/jetc/barant-site/assets/images/barant_web_logo.png b/jetc/barant-site/assets/images/barant_web_logo.png
new file mode 100644
index 0000000..0650af1
Binary files /dev/null and b/jetc/barant-site/assets/images/barant_web_logo.png differ
diff --git a/jetc/barant-site/assets/images/beep-07a.wav b/jetc/barant-site/assets/images/beep-07a.wav
new file mode 100644
index 0000000..015e1f6
Binary files /dev/null and b/jetc/barant-site/assets/images/beep-07a.wav differ
diff --git a/jetc/barant-site/assets/images/beep-08b.wav b/jetc/barant-site/assets/images/beep-08b.wav
new file mode 100644
index 0000000..24a13d4
Binary files /dev/null and b/jetc/barant-site/assets/images/beep-08b.wav differ
diff --git a/jetc/barant-site/assets/images/blank_collection_cover.png b/jetc/barant-site/assets/images/blank_collection_cover.png
new file mode 100644
index 0000000..99fbccb
Binary files /dev/null and b/jetc/barant-site/assets/images/blank_collection_cover.png differ
diff --git a/jetc/barant-site/assets/images/documents-page.js b/jetc/barant-site/assets/images/documents-page.js
new file mode 100644
index 0000000..b5ec9ee
--- /dev/null
+++ b/jetc/barant-site/assets/images/documents-page.js
@@ -0,0 +1,96 @@
+(function() {
+ var dropzone = document.getElementById('documents-dropzone');
+ var fileInput = document.getElementById('documents-file-input');
+ var listBody = document.getElementById('documents-list-body');
+ if (!dropzone || !listBody)
+ return;
+
+ function escapeHtml(s) {
+ var div = document.createElement('div');
+ div.textContent = s == null ? '' : String(s);
+ return div.innerHTML;
+ }
+
+ function post(path, params) {
+ return fetch(path, {
+ method: 'POST',
+ credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: new URLSearchParams(params || {}).toString()
+ }).then(function(r) { return r.json(); });
+ }
+
+ function loadList() {
+ post('/documents/api/list', {}).then(function(res) {
+ if (!res.ok || !res.documents.length) {
+ listBody.innerHTML = 'No documents yet - upload your first one above.
';
+ return;
+ }
+ var html = 'Name Uploaded ';
+ for (var i = 0; i < res.documents.length; i++) {
+ var doc = res.documents[i];
+ html += '' +
+ '' + escapeHtml(doc.name) + ' ' +
+ '' + escapeHtml(doc.created) + ' ' +
+ 'Download ' +
+ 'Delete ' +
+ ' ';
+ }
+ html += '
';
+ listBody.innerHTML = html;
+
+ var actions = listBody.querySelectorAll('.documents-action');
+ for (var j = 0; j < actions.length; j++) {
+ actions[j].addEventListener('click', function() {
+ var id = this.getAttribute('data-id');
+ if (this.getAttribute('data-action') === 'download') {
+ window.fileManagerDownload && window.fileManagerDownload(id, this.getAttribute('data-name'));
+ } else {
+ post('/documents/api/delete', { id: id }).then(function(res) {
+ if (res.ok)
+ loadList();
+ });
+ }
+ });
+ }
+ }).catch(function() {
+ listBody.innerHTML = 'Could not load your documents.
';
+ });
+ }
+
+ function uploadFiles(files) {
+ if (!files || !files.length)
+ return;
+ if (window.fileManagerUpload)
+ window.fileManagerUpload(files);
+ }
+
+ dropzone.addEventListener('click', function() {
+ fileInput.click();
+ });
+ fileInput.addEventListener('change', function() {
+ uploadFiles(fileInput.files);
+ fileInput.value = '';
+ });
+ dropzone.addEventListener('dragover', function(e) {
+ e.preventDefault();
+ dropzone.classList.add('documents-dropzone-active');
+ });
+ dropzone.addEventListener('dragleave', function() {
+ dropzone.classList.remove('documents-dropzone-active');
+ });
+ dropzone.addEventListener('drop', function(e) {
+ e.preventDefault();
+ dropzone.classList.remove('documents-dropzone-active');
+ uploadFiles(e.dataTransfer.files);
+ });
+
+ // The site-wide file manager (documents.js, loaded once via
+ // mainpagetemplate.jet) dispatches this after a successful upload -
+ // listening here (rather than only refreshing right after our own
+ // uploadFiles() calls) also picks up uploads that were still running
+ // when this page was navigated to via spa.js.
+ document.addEventListener('documents:changed', loadList);
+
+ loadList();
+})();
diff --git a/jetc/barant-site/assets/images/documents.js b/jetc/barant-site/assets/images/documents.js
new file mode 100644
index 0000000..a2f70e8
--- /dev/null
+++ b/jetc/barant-site/assets/images/documents.js
@@ -0,0 +1,178 @@
+// Site-wide file transfer manager - loaded once via mainpagetemplate.jet
+// (see its own script tag, outside #content) so the floating panel and
+// any in-progress transfer survive spa.js's own #content-only swaps
+// when navigating to a different page mid-upload/download.
+(function() {
+ var panel = document.getElementById('filemgr-panel');
+ if (!panel)
+ return;
+
+ var toggle = document.getElementById('filemgr-toggle');
+ var badge = document.getElementById('filemgr-badge');
+ var list = document.getElementById('filemgr-list');
+ var clearBtn = document.getElementById('filemgr-clear');
+
+ var transfers = {};
+ var nextId = 1;
+
+ function escapeHtml(s) {
+ var div = document.createElement('div');
+ div.textContent = s == null ? '' : String(s);
+ return div.innerHTML;
+ }
+
+ function updateBadge() {
+ var activeCount = 0;
+ for (var k in transfers) {
+ if (transfers[k].status === 'active')
+ activeCount++;
+ }
+ badge.textContent = activeCount > 0 ? String(activeCount) : '';
+ }
+
+ function removeEmptyPlaceholder() {
+ var empty = document.getElementById('filemgr-empty');
+ if (empty)
+ empty.remove();
+ }
+
+ function showEmptyPlaceholderIfNeeded() {
+ if (Object.keys(transfers).length === 0 && !document.getElementById('filemgr-empty'))
+ list.innerHTML = 'No transfers yet.
';
+ }
+
+ var UPLOAD_ICON = ' ';
+ var DOWNLOAD_ICON = ' ';
+
+ function addTransfer(name, direction) {
+ var id = 'xfer' + (nextId++);
+ removeEmptyPlaceholder();
+
+ var el = document.createElement('div');
+ el.className = 'filemgr-item';
+ el.innerHTML =
+ '' +
+ '' + (direction === 'upload' ? UPLOAD_ICON : DOWNLOAD_ICON) + ' ' +
+ '' + escapeHtml(name) + ' ' +
+ '0% ' +
+ '
' +
+ '';
+ list.insertBefore(el, list.firstChild);
+
+ transfers[id] = { el: el, status: 'active' };
+ panel.classList.add('filemgr-expanded');
+ updateBadge();
+ return id;
+ }
+
+ function updateProgress(id, percent) {
+ var t = transfers[id];
+ if (!t)
+ return;
+ t.el.querySelector('.filemgr-progress-fill').style.width = percent + '%';
+ t.el.querySelector('.filemgr-item-status').textContent = percent + '%';
+ }
+
+ function markDone(id, statusText) {
+ var t = transfers[id];
+ if (!t)
+ return;
+ t.status = 'done';
+ t.el.classList.add('filemgr-done');
+ t.el.querySelector('.filemgr-progress-fill').style.width = '100%';
+ t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Done';
+ updateBadge();
+ }
+
+ function markFailed(id, statusText) {
+ var t = transfers[id];
+ if (!t)
+ return;
+ t.status = 'failed';
+ t.el.classList.add('filemgr-failed');
+ t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Failed';
+ updateBadge();
+ }
+
+ // Uploads a FileList/array of File objects, each as its own
+ // concurrent XHR request - the browser's own HTTP connection pooling
+ // runs several at once, tracked here as separate panel entries with
+ // independent progress via xhr.upload's own progress event (fetch()
+ // has no equivalent for the request/upload side, only the response).
+ window.fileManagerUpload = function(files) {
+ for (var i = 0; i < files.length; i++) {
+ (function(file) {
+ var id = addTransfer(file.name, 'upload');
+ var xhr = new XMLHttpRequest();
+ transfers[id].xhr = xhr;
+ xhr.open('POST', '/documents/api/upload');
+ xhr.upload.addEventListener('progress', function(e) {
+ if (e.lengthComputable)
+ updateProgress(id, Math.round((e.loaded / e.total) * 100));
+ });
+ xhr.addEventListener('load', function() {
+ var res = null;
+ try { res = JSON.parse(xhr.responseText); } catch (e) {}
+ if (xhr.status === 200 && res && res.ok) {
+ markDone(id);
+ document.dispatchEvent(new CustomEvent('documents:changed'));
+ } else {
+ markFailed(id, (res && res.error) || 'Failed');
+ }
+ });
+ xhr.addEventListener('error', function() { markFailed(id); });
+ var fd = new FormData();
+ fd.append('name', file.name);
+ fd.append('filedata', file);
+ xhr.send(fd);
+ })(files[i]);
+ }
+ };
+
+ // Downloads a document by id via XHR (responseType 'blob') instead
+ // of a plain so it gets real tracked progress in the same
+ // panel as uploads, then hands the fetched bytes to the browser's
+ // normal save flow via a Blob URL + synthetic click.
+ window.fileManagerDownload = function(id, name) {
+ var xferId = addTransfer(name, 'download');
+ var xhr = new XMLHttpRequest();
+ transfers[xferId].xhr = xhr;
+ xhr.open('GET', '/documents/download?id=' + encodeURIComponent(id));
+ xhr.responseType = 'blob';
+ xhr.addEventListener('progress', function(e) {
+ if (e.lengthComputable)
+ updateProgress(xferId, Math.round((e.loaded / e.total) * 100));
+ });
+ xhr.addEventListener('load', function() {
+ if (xhr.status !== 200) {
+ markFailed(xferId);
+ return;
+ }
+ var blobUrl = URL.createObjectURL(xhr.response);
+ var a = document.createElement('a');
+ a.href = blobUrl;
+ a.download = name;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ setTimeout(function() { URL.revokeObjectURL(blobUrl); }, 30000);
+ markDone(xferId);
+ });
+ xhr.addEventListener('error', function() { markFailed(xferId); });
+ xhr.send();
+ };
+
+ toggle.addEventListener('click', function() {
+ panel.classList.toggle('filemgr-expanded');
+ });
+
+ clearBtn.addEventListener('click', function() {
+ for (var k in transfers) {
+ if (transfers[k].status !== 'active') {
+ transfers[k].el.remove();
+ delete transfers[k];
+ }
+ }
+ showEmptyPlaceholderIfNeeded();
+ });
+})();
diff --git a/jetc/barant-site/assets/images/edit.png b/jetc/barant-site/assets/images/edit.png
new file mode 100644
index 0000000..787c827
Binary files /dev/null and b/jetc/barant-site/assets/images/edit.png differ
diff --git a/jetc/barant-site/assets/images/email.js b/jetc/barant-site/assets/images/email.js
new file mode 100644
index 0000000..8350cc3
--- /dev/null
+++ b/jetc/barant-site/assets/images/email.js
@@ -0,0 +1,778 @@
+(function() {
+ var app = document.getElementById('email-app');
+ if (!app) return;
+
+ // Small inline-SVG icon set for the toolbar/action buttons below -
+ // stroke="currentColor" so each one just inherits its 's own
+ // text color (the site's gold theme, see .icon-btn in email.page's
+ // own
+
+
+
+
+
+
+
+
+
Select a message to read it.
+
+
+
+
+
diff --git a/jetc/barant-site/src/admin_invoices.page b/jetc/barant-site/src/admin-invoices.subpage
old mode 100755
new mode 100644
similarity index 61%
rename from jetc/barant-site/src/admin_invoices.page
rename to jetc/barant-site/src/admin-invoices.subpage
index 1b867e0..d11d89b
--- a/jetc/barant-site/src/admin_invoices.page
+++ b/jetc/barant-site/src/admin-invoices.subpage
@@ -1,14 +1,5 @@
-
-ADMIN INVOICES
-
-
-
-
Invoices
select invoices.id as X,invoices.id,user_id,name,invoices.created,amount,invoices.status from invoices join users on invoices.user_id=users.id
-
-
-
diff --git a/jetc/barant-site/src/admin-newemail.subpage b/jetc/barant-site/src/admin-newemail.subpage
new file mode 100644
index 0000000..8f9268a
--- /dev/null
+++ b/jetc/barant-site/src/admin-newemail.subpage
@@ -0,0 +1,81 @@
+
+
+
+ Connect to your mailbox first (see the Email section) before composing a message.
+
+ New Message
+
+
+
+
diff --git a/jetc/barant-site/src/admin-products.subpage b/jetc/barant-site/src/admin-products.subpage
new file mode 100644
index 0000000..bb15fd7
--- /dev/null
+++ b/jetc/barant-site/src/admin-products.subpage
@@ -0,0 +1,4 @@
+Products
+
+select sku as X,sku,type,description from products
+
diff --git a/jetc/barant-site/src/admin-users.subpage b/jetc/barant-site/src/admin-users.subpage
new file mode 100644
index 0000000..39d9f07
--- /dev/null
+++ b/jetc/barant-site/src/admin-users.subpage
@@ -0,0 +1,4 @@
+Users
+
+select id as X,email,name,type,created,status from users
+
diff --git a/jetc/barant-site/src/admin.jet b/jetc/barant-site/src/admin.jet
new file mode 100644
index 0000000..c623bd3
--- /dev/null
+++ b/jetc/barant-site/src/admin.jet
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetc/barant-site/src/admin.menu b/jetc/barant-site/src/admin.menu
new file mode 100755
index 0000000..332172d
--- /dev/null
+++ b/jetc/barant-site/src/admin.menu
@@ -0,0 +1,9 @@
+
+ EMAIL
+ INVOICES
+ PRODUCTS
+ USERS
+
+
+
+
diff --git a/jetc/barant-site/src/admin.page b/jetc/barant-site/src/admin.page
index bbcac46..ec239ab 100755
--- a/jetc/barant-site/src/admin.page
+++ b/jetc/barant-site/src/admin.page
@@ -1,17 +1,45 @@
-
+
ADMINISTRATION
+
+
+
+
+
+
+
+
+Requested page is not found.
+
+
+
+
+
+
+Requested page is not found.
+
+
+
+
+
+
+Requested page is not found.
+
+
+
+
+
+Requested page is not found.
+
-Users
-
-select id as X,email,name,type,created,status from users
-
-Products
-
-select sku as X,sku,type,description from products
-
-
+Select an item from the menu.
+
+
+
+
+
+
diff --git a/jetc/barant-site/src/barantmail.page b/jetc/barant-site/src/barantmail.page
deleted file mode 100755
index 5a8b5b1..0000000
--- a/jetc/barant-site/src/barantmail.page
+++ /dev/null
@@ -1,23 +0,0 @@
-
-BARANTMAIL OPEN SOURCE PROJECT
-
-
-In the year 2000 I was extremely frustrated with the state of e-amil
-systems available to Linux. Sendmail and q-mail were pretty much the two
-mainstays. sendmail was prohibitively complex for the newly emerging
-Internet method of using e-mail and q-mail was poorly maintained.
-As a result, I embarked on a mission to write my own e-mail system that used
-mysql as it's storage medium. I wanted to keep it simple and support
-multiple users and domains with the simplest of configurations imaginable.
-I also wanted a scalable multi-server capability.
-The result is BARANTMAIL.
-Features
-Features of BARANTMAIL are:
-
-Fully compliant e-mail system.
-Support for multiple domains.
-
-
-
-
-
diff --git a/jetc/barant-site/src/barantstore.page b/jetc/barant-site/src/barantstore.page
new file mode 100644
index 0000000..f6b4edd
--- /dev/null
+++ b/jetc/barant-site/src/barantstore.page
@@ -0,0 +1,42 @@
+
+BARANTSTORE
+
+
+
+BarantStore
+A demonstration storefront built on BARANT's own e-commerce
+platform - browse and purchase real products below.
+
+
+select product_list.sku as sku, product_list.sku_prefix as sku_prefix,
+ product_list.description as description, product_list.price as price,
+ products.image as image
+ from product_list
+ join products on products.sku=product_list.sku_prefix
+ order by product_list.category, product_list.description
+ $[sqlerror]
+
+
+
+
+
+
+
+
$[.description]
+
$$[.price]
+
Add to Cart
+
+
+
+
+No products available right now.
+
+
+
+
diff --git a/jetc/barant-site/src/cart.jet b/jetc/barant-site/src/cart.jet
new file mode 100644
index 0000000..b2c5910
--- /dev/null
+++ b/jetc/barant-site/src/cart.jet
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetc/barant-site/src/createhtmleditor.proc b/jetc/barant-site/src/createhtmleditor.proc
new file mode 100644
index 0000000..7bcd608
--- /dev/null
+++ b/jetc/barant-site/src/createhtmleditor.proc
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/jetc/barant-site/src/documents.jet b/jetc/barant-site/src/documents.jet
new file mode 100644
index 0000000..bb2edd2
--- /dev/null
+++ b/jetc/barant-site/src/documents.jet
@@ -0,0 +1,2 @@
+#api GET /documents/download
+Not authorized.select id,name from documents where id=$[id] and user_id=$[@_login_id] $[_filedata]Document not found. Unknown route.
\ No newline at end of file
diff --git a/jetc/barant-site/src/documents.page b/jetc/barant-site/src/documents.page
index bdce1cb..f8b7f58 100755
--- a/jetc/barant-site/src/documents.page
+++ b/jetc/barant-site/src/documents.page
@@ -2,15 +2,35 @@
Document Management
@@ -23,5 +43,14 @@ upon department for internal documents use. Or if you are a service
group you can establish accounts and store correspondence and
documents for any activity with that account.
+
+
+
+
+Drop files here, or click to choose files to upload
+
+
+
+
-
\ No newline at end of file
+
diff --git a/jetc/barant-site/src/documents_api.jet b/jetc/barant-site/src/documents_api.jet
new file mode 100644
index 0000000..4eb30ba
--- /dev/null
+++ b/jetc/barant-site/src/documents_api.jet
@@ -0,0 +1,52 @@
+#api POST /documents/api/upload
+#api POST /documents/api/delete
+#api POST /documents/api/list
+
+
+
+
+
+
+
+
+{"ok":false,"error":"Not authorized."}
+
+
+{"ok":false,"error":"Name is required."}
+
+ insert into documents set user_id=$[@_login_id], name="$[name]", created=now()
+ select last_insert_id() as document_id
+
+{"ok":true,"id":$[document_id],"name":"$[name]"}
+
+
+
+
+
+
+{"ok":false,"error":"Not authorized."}
+
+ select id from documents where id=$[id] and user_id=$[@_login_id]
+
+ delete from documents where id=$[id] and user_id=$[@_login_id]
+{"ok":true}
+
+{"ok":false,"error":"Document not found."}
+
+
+
+
+
+
+{"ok":false,"error":"Not authorized."}
+
+ select id,name,created from documents where user_id=$[@_login_id] order by created desc
+
+{"ok":true,"documents":[, {"id":$[.id],"name":"$[.name]","created":"$[.created]"} ]}
+
+
+{"ok":false,"error":"Unknown route."}
+
+
+
+
diff --git a/jetc/barant-site/src/documents_delete.proc b/jetc/barant-site/src/documents_delete.proc
new file mode 100644
index 0000000..a268a15
--- /dev/null
+++ b/jetc/barant-site/src/documents_delete.proc
@@ -0,0 +1,11 @@
+
+
+
+ select id from documents where id=$[delete_id] and user_id=$[@_login_id] $[sqlerror]
+
+ delete from documents where id=$[delete_id] and user_id=$[@_login_id] $[sqlerror]
+
+
+
+
+
diff --git a/jetc/barant-site/src/documents_upload.proc b/jetc/barant-site/src/documents_upload.proc
new file mode 100644
index 0000000..2ada2f6
--- /dev/null
+++ b/jetc/barant-site/src/documents_upload.proc
@@ -0,0 +1,10 @@
+
+
+
+ insert into documents set user_id=$[@_login_id], name="$[name]", created=now() $[sqlerror]
+ select last_insert_id() as document_id
+
+
+
+
+
diff --git a/jetc/barant-site/src/editproduct.page b/jetc/barant-site/src/editproduct.page
new file mode 100644
index 0000000..325c618
--- /dev/null
+++ b/jetc/barant-site/src/editproduct.page
@@ -0,0 +1,31 @@
+
+EDIT PRODUCT
+
+
+
+
+
+select * from products where sku='$[_product_sku]' and sku_suffix='' $[sqlerror]
+
+Product $[1.sku]
+
+ Type:
+
+ selected>PHYSICAL
+ selected>STREAMING
+ selected>SUBSCRIPTION
+ selected>FILE
+
+
+
+
+
+
+
+ Update Product
+
+
+
+
+
+
diff --git a/jetc/barant-site/src/email.jet b/jetc/barant-site/src/email.jet
new file mode 100644
index 0000000..1fa0525
--- /dev/null
+++ b/jetc/barant-site/src/email.jet
@@ -0,0 +1,178 @@
+#api POST /email/connect
+#api POST /email/disconnect
+#api POST /email/list
+#api POST /email/message
+#api POST /email/flag
+#api POST /email/delete
+#api POST /email/trash
+#api POST /email/folders
+#api POST /email/search
+#api POST /email/send
+#api POST /email/accounts
+#api POST /email/switchaccount
+
+
+
+
+
+
+
+
+
+
+{"ok":true}
+
+
+{"ok":false,"error":"$[_imaperr]"}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{"ok":true,"active":""}
+
+
+
+
+
+
+{"ok":true,"active":"$[_next_user]"}
+
+
+
+
+{"ok":true,"active":""}
+
+
+
+
+
+{"ok":false,"error":"Not connected to a mailbox."}
+
+
+
+
+
+
+
+
+
+
+
+
+
+{"ok":false,"error":"$[_imaperr]"}
+
+
+
+
+
+
+
+
+
+{"ok":false,"error":"$[_imaperr]"}
+
+
+
+
+
+
+
+
+
+{"ok":false,"error":"$[_imaperr]"}
+
+
+
+
+
+
+
+
+
+{"ok":false,"error":"$[_imaperr]"}
+
+
+
+
+
+
+
+
+
+{"ok":false,"error":"$[_imaperr]"}
+
+
+
+
+
+
+
+
+
+{"ok":false,"error":"$[_imaperr]"}
+
+
+
+
+
+
+
+
+
+{"ok":false,"error":"$[_imaperr]"}
+
+
+
+
+
+
+
+{"ok":false,"error":"$[_smtperr]"}
+
+
+
+
+
+
+
+
+
+
+{"ok":false,"error":"That account is not connected."}
+
+
+
+
+{"ok":true}
+
+
+
+{"ok":false,"error":"$[_switcherr]"}
+
+
+
+{"ok":false,"error":"Unknown route."}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetc/barant-site/src/email.menu b/jetc/barant-site/src/email.menu
new file mode 100644
index 0000000..a48c183
--- /dev/null
+++ b/jetc/barant-site/src/email.menu
@@ -0,0 +1,17 @@
+
+
+
+
+
+
++ Add Account
+
diff --git a/jetc/barant-site/src/htmleditor.jet b/jetc/barant-site/src/htmleditor.jet
new file mode 100644
index 0000000..6c9697f
--- /dev/null
+++ b/jetc/barant-site/src/htmleditor.jet
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetc/barant-site/src/htmleditor.page b/jetc/barant-site/src/htmleditor.page
new file mode 100644
index 0000000..4923ed5
--- /dev/null
+++ b/jetc/barant-site/src/htmleditor.page
@@ -0,0 +1,154 @@
+
+HTML EDITOR
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
SITE .JET FILES
+
These are the real source files compiled into this site's own routes. Editing and saving one overwrites it directly - a timestamped backup is made automatically before each save.
+
+$[filelist]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetc/barant-site/src/invoices.menu b/jetc/barant-site/src/invoices.menu
index 5a07f12..7a635b5 100755
--- a/jetc/barant-site/src/invoices.menu
+++ b/jetc/barant-site/src/invoices.menu
@@ -1,6 +1,6 @@
select * from invoices where user_id=$[_user_id] $[sqlerror]
- |$[created]|
+ |$[.created]|
diff --git a/jetc/barant-site/src/jet_overview.section b/jetc/barant-site/src/jet_overview.section
index e32b8a5..07e158e 100755
--- a/jetc/barant-site/src/jet_overview.section
+++ b/jetc/barant-site/src/jet_overview.section
@@ -27,17 +27,57 @@ to.
many expression handling functions are dependant on the mysql library
functions. Any expression that works in mysql will work in JET.
-JET Libraries
-You can make JET scripts and compile them into super fast
-libraries. Scripts can be anything from web page functions and
-concealers to session control, credit card processing and just about
-anything. Libraries add some performance enhancements by pre parsing
-many of the elements in the script.
+One Language, Two Speeds: The JET 2.0 Interpreter and Compiler
-Libraries allow you to make your own tags and implement them in
-the scripts. JET does not have JSON or XML parsing but using a
-libraries you can develop an XML parser operation that usually
-suffices for most script processing.
+
Every JET script runs two ways, and you never have to choose in
+advance. Drop a file on disk and JET interprets it on the spot —
+edit it, refresh, see the change. No build step, no deploy pipeline,
+no waiting. That's the interpreter, and it's what makes JET feel less
+like "compiling a website" and more like sketching one.
+
+Then, when a page is ready to carry real traffic, the exact same
+source compiles straight down to native machine code with the JET
+compiler, jetc . Not bytecode. Not a virtual machine sitting
+in the middle slowing things down. Real, compiled C++ — the
+tags you wrote resolve once, at build time, instead of being
+re-parsed on every single request.
+
+That's the pitch in one line: write it fast, then make it fast.
+
+What jetc gives you:
+
+Genuine compiled-code speed. A compiled JET module isn't
+an interpreted script pretending to be quick — it's a shared
+library. Tag logic that would normally be walked and evaluated on
+every hit is already resolved into executable code before the first
+request ever arrives.
+
+Zero-downtime deploys, by design. Compiled modules are
+loaded live and swapped out while the server keeps running. A
+request already in flight finishes on the version it started with;
+the very next request lands on the new one. No restart, no dropped
+connections, no maintenance window.
+
+Routes that declare themselves. A compiled module carries
+its own API routes right inside the source file. Drop it in the
+plugin directory and it's live — there's no central routing
+table to hand-edit and no separate registration step to forget.
+
+Builds that only do the work that changed. The compiler
+toolchain tracks what's actually been touched and skips
+recompiling everything else, so a large site with dozens of modules
+still builds in seconds, not minutes.
+
+Shared code costs nothing at runtime. Common tag
+definitions and includes get inlined directly into the compiled
+output at build time — you write clean, reusable JET source,
+and the compiler makes sure reusing it is free.
+
+
+The interpreter and the compiler aren't two different products
+you have to pick between — they're two gears on the same
+engine. Prototype in the interpreter, ship in the compiler, and
+never rewrite a single tag to get there.
Session Control (SESSIONCTL) Library
For web sites, a session control library is provided with JET and
diff --git a/jetc/barant-site/src/login.jet b/jetc/barant-site/src/login.jet
new file mode 100644
index 0000000..63b20d6
--- /dev/null
+++ b/jetc/barant-site/src/login.jet
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetc/barant-site/src/login.menu b/jetc/barant-site/src/login.menu
index 8638fd2..dc3dbaf 100755
--- a/jetc/barant-site/src/login.menu
+++ b/jetc/barant-site/src/login.menu
@@ -1,5 +1,5 @@
- Welcome $[_login_name]
+ Welcome $[_login_name]
Logout
diff --git a/jetc/barant-site/src/main.menu b/jetc/barant-site/src/main.menu
new file mode 100644
index 0000000..b2afa6e
--- /dev/null
+++ b/jetc/barant-site/src/main.menu
@@ -0,0 +1,7 @@
+
+ SERVICES
+ HOSTING
+ SIP TRUNKING
+ OPEN SOURCE
+ CONTACT
+
diff --git a/jetc/barant-site/src/mainpagetemplate.jet b/jetc/barant-site/src/mainpagetemplate.jet
new file mode 100644
index 0000000..9998d10
--- /dev/null
+++ b/jetc/barant-site/src/mainpagetemplate.jet
@@ -0,0 +1,149 @@
+#api GET /
+#api GET /{page}
+#api GET /{page}/{subpage}
+#api POST /
+#api POST /{page}
+#api POST /{page}/{subpage}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Page not found.
+
+
+
+
+
+
+
+
+
+Page not found.
+
+$[_BODY]
+
+
+
+
+
+
+
+
+
+
+BARANT Technology Group
+
+
+
+
+ " enctype="multipart/form-data">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
BARANT TECHNOLOGY GROUP
+
+
+
+
+
+
+
+
+
+
+
+
+
+Page not found.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Page not found.
+
+
+
diff --git a/jetc/barant-site/src/menu.bar b/jetc/barant-site/src/menu.bar
index 5ae43bf..59b768d 100755
--- a/jetc/barant-site/src/menu.bar
+++ b/jetc/barant-site/src/menu.bar
@@ -10,6 +10,7 @@
HOME
SERVICES
HOSTING
+ CREATE STORE
DOCUMENT MGMT
SIP TRUNKING
VST
@@ -26,10 +27,7 @@
ACCOUNT
- EMAIL
-
- ADMIN
-
+ ADMIN
LOGIN
diff --git a/jetc/barant-site/src/modules.page b/jetc/barant-site/src/modules.page
new file mode 100644
index 0000000..ba20d3f
--- /dev/null
+++ b/jetc/barant-site/src/modules.page
@@ -0,0 +1,27 @@
+
+LOADED MODULES
+
+
+
+This APIServer process is running as a small core loader with the
+real functionality split into separately compiled .jtc modules.
+Each one is dlopen()'d straight off disk by ApiModuleWatcher, and
+watched live — drop a new build into the plugin directory and it's
+serving requests within moments, with zero downtime and no restart.
+
+Below is the actual, live list of every compiled module currently
+attached to this process, read directly off disk at the moment this
+page rendered.
+
+
+Module File
+
+
+$[modname] $[filename]
+
+
+
+
+
+
+
diff --git a/jetc/barant-site/src/asm1802.page b/jetc/barant-site/src/opensource-asm1802.subpage
old mode 100755
new mode 100644
similarity index 95%
rename from jetc/barant-site/src/asm1802.page
rename to jetc/barant-site/src/opensource-asm1802.subpage
index 530997b..7e4a890
--- a/jetc/barant-site/src/asm1802.page
+++ b/jetc/barant-site/src/opensource-asm1802.subpage
@@ -1,6 +1,3 @@
-
-RCA1802 Micro-Controller Assembler/Emulator
-
The RCA1802 Micro-Controller
@@ -42,11 +39,8 @@ visibility and manipulation.
-We are working to provide a hardware kit for the controller device.
+We are working to provide a hardware kit for the controller device.
Current schematics are being layed out using Kicad. I have a full git
change environment going for the Kicad project as well as the source
code for the assembler and emulator components.
-
-
-
\ No newline at end of file
diff --git a/jetc/barant-site/src/opensource-barantmail.subpage b/jetc/barant-site/src/opensource-barantmail.subpage
new file mode 100644
index 0000000..d6bfe0b
--- /dev/null
+++ b/jetc/barant-site/src/opensource-barantmail.subpage
@@ -0,0 +1,53 @@
+
+In the year 2000 I was extremely frustrated with the state of e-amil
+systems available to Linux. Sendmail and q-mail were pretty much the two
+mainstays. sendmail was prohibitively complex for the newly emerging
+Internet method of using e-mail and q-mail was poorly maintained.
+As a result, I embarked on a mission to write my own e-mail system that used
+mysql as it's storage medium. I wanted to keep it simple and support
+multiple users and domains with the simplest of configurations imaginable.
+I also wanted a scalable multi-server capability.
+The result is BARANTMAIL.
+Features
+BARANTMAIL is a complete mail server implementation, written from
+scratch in C++ - no dependency on Sendmail, Postfix, Dovecot, or any
+other existing mail server software.
+
+Protocols
+
+SMTP (port 25) plus authenticated SMTP-over-TLS submission (port 465).
+IMAP4rev1 over TLS (port 993) - LOGIN, SELECT, FETCH/UID FETCH, STORE/UID STORE, COPY/UID COPY, EXPUNGE, SEARCH/UID SEARCH, SUBSCRIBE, NOOP and more.
+POP3 over TLS (port 995) - USER/PASS, STAT, LIST, RETR, DELE, TOP, UIDL.
+
+
+Security
+
+TLS/SSL on every protocol, backed by OpenSSL and real CA-issued certificates.
+DKIM signing on outbound mail.
+SPF verification on inbound mail.
+
+
+Spam Filtering
+
+Every inbound message is scanned by SpamAssassin (spoken directly over the spamd protocol, not shelled out to a binary) on the way into a mailbox.
+Messages scoring above the configured threshold are automatically filed into a dedicated Spam folder rather than the Inbox - not just tagged and left mixed in with everything else.
+
+
+Multi-Tenancy
+
+Multiple domains on one server (virtual hosting).
+Multiple independent mailboxes per domain, each with its own Inbox, Spam, and Trash folders, plus support for any additional folder a client creates.
+
+
+Architecture
+
+Asynchronous, EPoll-based I/O - handles many concurrent connections without a thread per connection.
+Queue-based SMTP delivery - inbound mail is queued to disk, then either filed locally or relayed onward over SMTP on a background thread, so a slow remote host never blocks the server.
+
+
+Companion Webmail Client
+A full browser-based email client (see Email
+under the Admin section) is built directly against BARANTMAIL's own IMAP
+implementation - real proof the protocol support is complete enough to
+drive a real application, not just a demonstration.
+
diff --git a/jetc/barant-site/src/jet.page b/jetc/barant-site/src/opensource-jet.subpage
old mode 100755
new mode 100644
similarity index 68%
rename from jetc/barant-site/src/jet.page
rename to jetc/barant-site/src/opensource-jet.subpage
index 9ca78f2..469adc7
--- a/jetc/barant-site/src/jet.page
+++ b/jetc/barant-site/src/opensource-jet.subpage
@@ -1,13 +1,6 @@
-
-barant.com - JET Extension Tags
-
-
-
-
-
diff --git a/jetc/barant-site/src/opensource-overview.subpage b/jetc/barant-site/src/opensource-overview.subpage
new file mode 100644
index 0000000..b53a09f
--- /dev/null
+++ b/jetc/barant-site/src/opensource-overview.subpage
@@ -0,0 +1,49 @@
+
+Open Source Projects
+We have several open source projects that we have created.
+
+
+
+ JET - JET Extension Tags provides an amazingly versatile and
+simple mechanism for generating output for muliple uses. Originally
+designed in the early days of the Internet for generating web pages
+containing dynamic data it has been adapted to many uses from Asterisk
+dial plans to other generated output that requires templated protocols
+with dynamic data from a mysql database. Jeff Gonzalez added the
+ability to manage web site sessions and pretty URLs to JET with his
+session manager library.
+
+ BARANTMAIL - Designed out of frustration with qmail a decade ago
+BARANTMAIL was developed on top of C++ and mySQL to provide an
+extremely super simple e-mail server for the open-source
+community.
+
+ SYNTHMAX - I have been developing a fully modular synthesizer for
+use in Linux. This is not your usual plaything and maybe someday will
+be used professionally in my keyboard products.
+
+ C++ SERVER FRAMEWORK - A core epoll server code for developing
+all sorts of high performance TCP and UDP servers. Generic socket
+objects allow tying into epoll handling for any socket based
+communications including timers, files and network sockets.
+Basic functional server frameworks are provided for:
+
+
+ HTTP(S) Server
+ Stream Server
+ E-Mail Server
+ Chat Server
+
+
+
+
+RCA1802 Micro-Controller
+Emulator - A fully operational assembler
+and emulation environment for the development and deployment of
+firmware code utilizing the RCA1802 micro-controller chip.
+
+
+
+Check out our git server at git.barant.com .
+
+
diff --git a/jetc/barant-site/src/serverframework.page b/jetc/barant-site/src/opensource-serverframework.subpage
old mode 100755
new mode 100644
similarity index 90%
rename from jetc/barant-site/src/serverframework.page
rename to jetc/barant-site/src/opensource-serverframework.subpage
index df9b6a9..f3b5d7a
--- a/jetc/barant-site/src/serverframework.page
+++ b/jetc/barant-site/src/opensource-serverframework.subpage
@@ -1,6 +1,3 @@
-
-Server Core Framework
-
It is finally here! The Server Core Framework is now available. The
framework offers support for just about any server you can devise.
@@ -8,7 +5,7 @@ framework offers support for just about any server you can devise.
Now why would I want to go and build a server when there are all
sorts of frameworks out there for supporting server activity? All I
can say is top notch performance. The Server Core is all C++ and is
-built on the epoll structure of the Linux operating system.
+built on the epoll structure of the Linux operating system.
TCP and/or UDP server capabilities.
@@ -40,5 +37,3 @@ git clone https://barant.com/git@ServerCore.git
-
-
\ No newline at end of file
diff --git a/jetc/barant-site/src/opensource-synthmax.subpage b/jetc/barant-site/src/opensource-synthmax.subpage
new file mode 100644
index 0000000..ccfe054
--- /dev/null
+++ b/jetc/barant-site/src/opensource-synthmax.subpage
@@ -0,0 +1,5 @@
+
+I have been developing a fully modular synthesizer for
+use in Linux. This is not your usual plaything and maybe someday will
+be used professionally in my keyboard products.
+
diff --git a/jetc/barant-site/src/opensource.menu b/jetc/barant-site/src/opensource.menu
index c03e4b8..db7b8ba 100755
--- a/jetc/barant-site/src/opensource.menu
+++ b/jetc/barant-site/src/opensource.menu
@@ -1,7 +1,8 @@
- BARANTMAIL
- JET
- SYNTHMAX
- SERVER CORE FRAMEWORK
- ASM/MCH 1802 SIMULATOR
+ OVERVIEW
+ BARANTMAIL
+ JET
+ SYNTHMAX
+ SERVER CORE FRAMEWORK
+ ASM/MCH 1802 SIMULATOR
diff --git a/jetc/barant-site/src/opensource.page b/jetc/barant-site/src/opensource.page
index eccfcf2..6e53b72 100755
--- a/jetc/barant-site/src/opensource.page
+++ b/jetc/barant-site/src/opensource.page
@@ -1,54 +1,16 @@
OPEN SOURCE
-
- Open Source Projects
- We have several open source projects that we have created.
-
-
-
- JET - JET Extension Tags provides an amazingly versatile and
-simple mechanism for generating output for muliple uses. Originally
-designed in the early days of the Internet for generating web pages
-containing dynamic data it has been adapted to many uses from Asterisk
-dial plans to other generated output that requires templated protocols
-with dynamic data from a mysql database. Jeff Gonzalez added the
-ability to manage web site sessions and pretty URLs to JET with his
-session manager library.
-
- BARANTMAIL - Designed out of frustration with qmail a decade ago
-BARANTMAIL was developed on top of C++ and mySQL to provide an
-extremely super simple e-mail server for the open-source
-community.
-
- SYNTHMAX - I have been developing a fully modular synthesizer for
-use in Linux. This is not your usual plaything and maybe someday will
-be used professionally in my keyboard products.
-
- C++ SERVER FRAMEWORK - A core epoll server code for developing
-all sorts of high performance TCP and UDP servers. Generic socket
-objects allow tying into epoll handling for any socket based
-communications including timers, files and network sockets.
-Basic functional server frameworks are provided for:
-
-
- HTTP(S) Server
- Stream Server
- E-Mail Server
- Chat Server
-
-
-
-
- RCA1802 Micro-Controller
-Emulator - A fully operational assembler
-and emulation environment for the development and deployment of
-firmware code utilizing the RCA1802 micro-controller chip.
-
-
-
- Check out our git server at git.barant.com .
-
-
+
+
+
+
+
+
+
+That open source category doesn't exist.
+
+
+
diff --git a/jetc/barant-site/src/removecartitem.proc b/jetc/barant-site/src/removecartitem.proc
new file mode 100644
index 0000000..63fe5f2
--- /dev/null
+++ b/jetc/barant-site/src/removecartitem.proc
@@ -0,0 +1 @@
+delete from cart_item where session_uuid='$[_session_id]' and sku='$[sku]' $[sqlerror]
diff --git a/jetc/barant-site/src/savehtmleditor.proc b/jetc/barant-site/src/savehtmleditor.proc
new file mode 100644
index 0000000..fc32e8a
--- /dev/null
+++ b/jetc/barant-site/src/savehtmleditor.proc
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetc/barant-site/src/selectproduct.proc b/jetc/barant-site/src/selectproduct.proc
new file mode 100644
index 0000000..a52583e
--- /dev/null
+++ b/jetc/barant-site/src/selectproduct.proc
@@ -0,0 +1,3 @@
+
+ $[sku]
+
diff --git a/jetc/barant-site/src/store-createstore.subpage b/jetc/barant-site/src/store-createstore.subpage
new file mode 100644
index 0000000..137d138
--- /dev/null
+++ b/jetc/barant-site/src/store-createstore.subpage
@@ -0,0 +1,12 @@
+
+Create Your Own Store
+BARANT Technology Group's e-commerce platform lets you build and run
+your own online store right here on this site - list physical
+products, digital downloads, subscriptions, or streaming content, and
+we handle checkout, payment processing, and order management for you.
+Whether you're selling a handful of items or running a full catalog,
+your store lives alongside everything else here - the same account,
+document storage, and support tools you already use.
+Want to see it in action? Take a look at BarantStore ,
+our own storefront, built on the same platform we're offering you.
+
diff --git a/jetc/barant-site/src/store.page b/jetc/barant-site/src/store.page
new file mode 100644
index 0000000..8f368c4
--- /dev/null
+++ b/jetc/barant-site/src/store.page
@@ -0,0 +1,10 @@
+
+STORE
+
+
+
+
+Page not found.
+
+
+
diff --git a/jetc/barant-site/src/update_product.proc b/jetc/barant-site/src/update_product.proc
new file mode 100644
index 0000000..d737a09
--- /dev/null
+++ b/jetc/barant-site/src/update_product.proc
@@ -0,0 +1,5 @@
+
+update products set type='$[type]', description='$[description]', text='$[text]', price=$[price], image='$[image]' where sku='$[_product_sku]' and sku_suffix='' $[sqlerror]
+
+
+
diff --git a/jetc/barant-site/src/updatecartqty.proc b/jetc/barant-site/src/updatecartqty.proc
new file mode 100644
index 0000000..163f2dd
--- /dev/null
+++ b/jetc/barant-site/src/updatecartqty.proc
@@ -0,0 +1,5 @@
+
+delete from cart_item where session_uuid='$[_session_id]' and sku='$[sku]' $[sqlerror]
+
+update cart_item set quantity=$[qty] where session_uuid='$[_session_id]' and sku='$[sku]' $[sqlerror]
+
diff --git a/jetc/build-assets.sh b/jetc/build-assets.sh
new file mode 100755
index 0000000..e6c6795
--- /dev/null
+++ b/jetc/build-assets.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+# Builds the hand-written assets.compiled.cpp module into assets.jtc.
+# Mirrors build-shared.sh's compile/link tail exactly (same object cache,
+# same libCoreUtils_pic.a, same link line) but skips the jetc --shared
+# codegen step since there is no .jet source for this module.
+set -e
+JC="$(cd "/home/barant/servercore-analysis/JetCore" && pwd)"
+CU="$JC/../CoreUtils"
+HERE="$JC/jetc"
+STEM="assets"
+OUTPUT="$1"
+
+if [ -z "$OUTPUT" ]; then
+ echo "usage: $0 "
+ exit 1
+fi
+
+echo "Building JetCore runtime objects as position-independent code..."
+list=""
+for f in "$JC"/*.cpp; do
+ base=$(basename "$f")
+ if [ "$base" = "jet-2.0.cpp" ]; then continue; fi
+ obj="$HERE/${base%.cpp}.pic.o"
+ if [ ! -f "$obj" ] || [ "$f" -nt "$obj" ]; then
+ g++ -g -fPIC -c -std=c++23 -I"$CU" -I"$JC" "$f" -o "$obj" &
+ fi
+ list="$list $obj"
+done
+
+cu_pic_list=""
+cu_rebuilt=false
+for f in "$CU"/*.cpp; do
+ base=$(basename "$f")
+ obj="$HERE/cu_${base%.cpp}.pic.o"
+ if [ ! -f "$obj" ] || [ "$f" -nt "$obj" ]; then
+ g++ -g -fPIC -c -std=c++23 -I"$CU" "$f" -o "$obj" &
+ cu_rebuilt=true
+ fi
+ cu_pic_list="$cu_pic_list $obj"
+done
+wait
+if [ "$cu_rebuilt" = true ] || [ ! -f "$HERE/libCoreUtils_pic.a" ]; then
+ ar rcs "$HERE/libCoreUtils_pic.a" $cu_pic_list
+fi
+
+g++ -g -fPIC -c -std=c++23 -I"$CU" -I"$JC" "$HERE/$STEM.compiled.cpp" -o "$HERE/$STEM.compiled.pic.o"
+
+echo "Linking $OUTPUT..."
+g++ -shared -fPIC -o "$OUTPUT" "$HERE/$STEM.compiled.pic.o" $list -std=c++23 \
+ -L"$HERE" -lCoreUtils_pic -lmysqlclient -lcrypto
+
+echo "Built $OUTPUT"
diff --git a/jetc/lowercase_jet.py b/jetc/lowercase_jet.py
new file mode 100644
index 0000000..11ccf19
--- /dev/null
+++ b/jetc/lowercase_jet.py
@@ -0,0 +1,261 @@
+"""
+Lowercases jet tag NAMES ( / ) and attribute NAMES
+(ATTR=) throughout a jet source file, WITHOUT touching:
+ - attribute VALUES (quoted or unquoted)
+ - $[...]/#[...] variable references anywhere (including nested ones,
+ and ones that appear inside attribute values)
+ - literal HTML/text content outside of tags
+
+Handles the same real-content wrinkles this session's own JetParser.cpp
+fixes had to: tags embedded inside a quoted attribute value (e.g.
+linkbutton's own HREF="javascript: ...... ..."), and
+both single and double quoted attribute values.
+
+This is a straightforward character-by-character scanner, not a regex -
+regex can't reliably track the nested quote/tag structure real content
+uses.
+"""
+import sys
+
+
+IDENT_CHARS = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")
+
+
+def skip_variable_ref(s, i, out):
+ """s[i] is '$' or '#', s[i+1] is '['. Copies the whole $[...]/#[...]
+ reference verbatim (including nested [...] refs inside it) to out,
+ returns the index just past the closing ']'."""
+ out.append(s[i])
+ out.append(s[i + 1])
+ i += 2
+ depth = 1
+ while i < len(s) and depth > 0:
+ if s[i] == '[':
+ depth += 1
+ out.append(s[i])
+ i += 1
+ elif s[i] == ']':
+ depth -= 1
+ out.append(s[i])
+ i += 1
+ else:
+ out.append(s[i])
+ i += 1
+ return i
+
+
+def is_var_start(s, i):
+ return i + 1 < len(s) and s[i] in '$#' and s[i + 1] == '['
+
+
+def looks_like_tag(s, i):
+ """s[i] is '<'. Peeks (without consuming) to decide whether this
+ really starts a jet tag, using the same rule Tag.cpp's own
+ constructor uses at runtime (Tag.cpp:50): read an identifier-char
+ run right after '<' (or after '' for a close tag), and only
+ treat it as a real tag if that run is immediately followed by
+ whitespace, '/' or '>'. A literal '<' embedded in JS/text (e.g.
+ console.log("<")) fails this and must be left untouched - real
+ content relies on exactly this rule to disambiguate."""
+ n = len(s)
+ j = i + 1
+ if j < n and s[j] == '/':
+ k = j + 1
+ while k < n and s[k] in IDENT_CHARS:
+ k += 1
+ if k == j + 1:
+ return False
+ return k < n and s[k] == '>'
+ if j >= n or not s[j].isalpha():
+ return False
+ k = j
+ while k < n and s[k] in IDENT_CHARS:
+ k += 1
+ return k < n and s[k] in ' \t\r\n/>'
+
+
+def skip_quoted_value(s, i, quote_char, out):
+ """s[i] is the opening quote character (already consumed by caller,
+ i.e. this points at the FIRST character of the value). Copies the
+ value verbatim, including any embedded ... constructs and
+ variable refs, stopping at (and consuming) the matching closing
+ quote. Returns the index just past the closing quote."""
+ while i < len(s):
+ if s[i] == quote_char:
+ out.append(s[i])
+ return i + 1
+ if is_var_start(s, i):
+ i = skip_variable_ref(s, i, out)
+ continue
+ if s[i] == '<' and looks_like_tag(s, i):
+ i = skip_embedded_tag(s, i, out)
+ continue
+ out.append(s[i])
+ i += 1
+ return i
+
+
+def skip_embedded_tag(s, i, out):
+ """s[i] is '<', starting a tag embedded inside a quoted attribute
+ value (or, via lowercase_source's own top-level loop, an ordinary
+ top-level tag). Lowercases its name and attribute names the same
+ way the top-level scanner does, then either stops after a
+ self-closing '/>' or, if it looks like it has its own body, copies
+ through to its own matching close tag verbatim (rare inside a
+ quoted value in real content; the void-tag list below covers the
+ common built-ins that never do). Returns the index just past this
+ tag's own closing '>' (and, for a real container tag, past its
+ matching close tag too)."""
+ out.append(s[i]) # '<'
+ i += 1
+ closing = False
+ if i < len(s) and s[i] == '/':
+ closing = True
+ out.append(s[i])
+ i += 1
+ name_start = i
+ while i < len(s) and s[i] in IDENT_CHARS:
+ i += 1
+ name = s[name_start:i]
+ out.append(name.lower())
+
+ if closing:
+ # just the name, then skip to '>'
+ while i < len(s) and s[i] != '>':
+ out.append(s[i])
+ i += 1
+ if i < len(s):
+ out.append(s[i])
+ i += 1
+ return i
+
+ # attribute region
+ self_closed = False
+ while i < len(s):
+ if s[i] in ' \t\r\n':
+ out.append(s[i])
+ i += 1
+ continue
+ if s[i] == '/' and i + 1 < len(s) and s[i + 1] == '>':
+ out.append('/>')
+ i += 2
+ self_closed = True
+ break
+ if s[i] == '>':
+ out.append('>')
+ i += 1
+ break
+ if s[i] in IDENT_CHARS:
+ attr_start = i
+ while i < len(s) and s[i] in IDENT_CHARS:
+ i += 1
+ attr_name = s[attr_start:i]
+ out.append(attr_name.lower())
+ # optional whitespace before '='
+ j = i
+ while j < len(s) and s[j] in ' \t\r\n':
+ j += 1
+ if j < len(s) and s[j] == '=':
+ out.append(s[i:j])
+ i = j
+ out.append('=')
+ i += 1
+ # optional whitespace after '='
+ while i < len(s) and s[i] in ' \t\r\n':
+ out.append(s[i])
+ i += 1
+ if i < len(s) and s[i] in ('"', "'"):
+ q = s[i]
+ out.append(q)
+ i += 1
+ i = skip_quoted_value(s, i, q, out)
+ else:
+ # unquoted value - copy verbatim to next
+ # whitespace/'>'//'
+ while i < len(s) and s[i] not in ' \t\r\n>/':
+ if is_var_start(s, i):
+ i = skip_variable_ref(s, i, out)
+ else:
+ out.append(s[i])
+ i += 1
+ continue
+ # anything unexpected - copy through defensively
+ out.append(s[i])
+ i += 1
+
+ void_tags = {
+ "set", "include", "cookie", "header", "read", "write", "call",
+ "system", "dump", "br", "img", "input", "hr", "meta", "link",
+ "area", "base", "col", "embed", "source", "track", "wbr",
+ "else",
+ }
+ if self_closed or name.lower() in void_tags:
+ return i
+
+ # real container tag embedded in a quoted value - copy through to
+ # its own matching close, tracking nesting of the same tag name.
+ depth = 1
+ nlower = name.lower()
+ while i < len(s) and depth > 0:
+ if is_var_start(s, i):
+ i = skip_variable_ref(s, i, out)
+ continue
+ if s[i] == '<':
+ # peek for open/close of the same tag name
+ j = i + 1
+ is_close = False
+ if j < len(s) and s[j] == '/':
+ is_close = True
+ j += 1
+ k = j
+ while k < len(s) and s[k] in IDENT_CHARS:
+ k += 1
+ token = s[j:k]
+ if not looks_like_tag(s, i):
+ out.append(s[i])
+ i += 1
+ continue
+ if token.lower() == nlower:
+ if is_close:
+ depth -= 1
+ else:
+ depth += 1
+ i = skip_embedded_tag(s, i, out)
+ continue
+ i = skip_embedded_tag(s, i, out)
+ continue
+ out.append(s[i])
+ i += 1
+ return i
+
+
+def lowercase_source(s):
+ out = []
+ i = 0
+ n = len(s)
+ while i < n:
+ if is_var_start(s, i):
+ i = skip_variable_ref(s, i, out)
+ continue
+ if s[i] == '<' and looks_like_tag(s, i):
+ i = skip_embedded_tag(s, i, out)
+ continue
+ out.append(s[i])
+ i += 1
+ return ''.join(out)
+
+
+def main():
+ if len(sys.argv) != 3:
+ print("usage: lowercase_jet.py ")
+ sys.exit(1)
+ with open(sys.argv[1], encoding="utf-8", errors="surrogateescape", newline="") as f:
+ content = f.read()
+ result = lowercase_source(content)
+ with open(sys.argv[2], "w", encoding="utf-8", errors="surrogateescape", newline="") as f:
+ f.write(result)
+ print("wrote", sys.argv[2], "(%d bytes -> %d bytes)" % (len(content), len(result)))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/jetc/strip_fragments.py b/jetc/strip_fragments.py
new file mode 100755
index 0000000..e5b6379
--- /dev/null
+++ b/jetc/strip_fragments.py
@@ -0,0 +1,96 @@
+"""
+Strips the dead SPA-fragment dual-render wrapper from a barant-site/jet/*.jet
+route file, leaving just the real page content (the former "_full_NAME" tag
+body), unwrapped, with no fragment-flag try/catch, no _frag_NAME tag
+definition, no dispatch , and no dead client-side SPA navigation script.
+
+Structure being removed (present in all 20 files, verified consistent):
+
+ #api GET /NAME
+ ......
+ ... fragment-only content, NEVER actually invoked any more since the
+ client-side SPA click handlers were already disabled earlier this
+ session ...
+
+ ... the REAL page content - full doctype/head/style/body wrapper ...
+