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 = ''; + for (var i = 0; i < res.documents.length; i++) { + var doc = res.documents[i]; + html += '' + + '' + + '' + + '' + + '' + + ''; + } + html += '
NameUploaded
' + escapeHtml(doc.name) + '' + escapeHtml(doc.created) + 'DownloadDelete
'; + 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 +
+ + +
+
+ $[@_imap_user] + + + + + + + + + +
+
+
+
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

+
+
$[@_imap_user]
+
+
+
+ + + + + +
+
+
+
+ + +
+
+
+
+ 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
+
Loading…
+
+ +
- \ 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: + + + + + + + +
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] +
+ + + + + + +
+ + + + + + +
+ +
+
+
$[letter_dir]/$[letter_file]
+
+
+
+
+
+
+
+   +
+ +
+
+
+
+
+
+
+
+
+ +
+ $[mhtml] +
+ +
+ + +
+ + Options + +
+ +
+ Show Grid
+ Snap To Grid
+ Grid Size:
+
+ +
+ +
+

NO DATA

+
+ +
+
+ + + +
+ Toolbar +
+
+
+ + + +
+ + Images + +
+
+
+
+
drinkwise_logo.png
+
+
+
+
dwbackground.png
+
+
+
+
+
+
+
+ + + +
+ +
+
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 + + + + +
/$[SUBPAGE]" enctype="multipart/form-data"> + + + + + + + + +
+
+
+ +
+
BARANT TECHNOLOGY GROUP
+
+
+
+
+ +
+
+
+ + +
+
+
+ + + +

Page not found.

+
+
+
+
+
+ +
+
+
+
+ + +
+
+
+ Transfers + Clear +
+
No transfers yet.
+
+
+ + + + + +
+ + + + + + +

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.

+ + + + + + + + +
ModuleFile
$[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 ''. 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 ... + + <_frag_NAME /><_full_NAME /> + +Kept: "#api GET /NAME\n" plus everything between "" +and its own matching close, MINUS the dead ", + re.DOTALL, + ) + new_content, n = pattern.subn("", content, count=1) + return new_content, n + + +def strip_fragment_wrapper(content, page_name): + lines = content.split("\n", 1) + assert lines[0].startswith("#api GET /"), f"unexpected first line: {lines[0]!r}" + api_line = lines[0] + rest = lines[1] if len(lines) > 1 else "" + + start_marker = ( + '' + '' + f'' + ) + if start_marker not in rest: + return None, f"start marker not found" + idx = rest.index(start_marker) + prefix_extra = rest[:idx] # should just be leading whitespace/newline, kept as-is + rest = rest[idx + len(start_marker):] + + mid_marker = f'' + if mid_marker not in rest: + return None, f"mid marker not found" + mid_idx = rest.index(mid_marker) + rest = rest[mid_idx + len(mid_marker):] + + end_pattern = re.compile( + r'' + rf'<_frag_{re.escape(page_name)} /><_full_{re.escape(page_name)} />\s*$' + ) + m = end_pattern.search(rest) + if not m: + return None, f"end marker not found" + full_content = rest[:m.start()] + + full_content, nscript = strip_dead_spa_script(full_content) + + result = api_line + "\n" + prefix_extra + full_content + return result, f"ok (removed {nscript} dead script block(s))" + + +def main(): + if len(sys.argv) != 4: + print("usage: strip_fragments.py ") + sys.exit(1) + with open(sys.argv[1], encoding="utf-8", newline="") as f: + content = f.read() + result, status = strip_fragment_wrapper(content, sys.argv[3]) + if result is None: + print(f"FAILED for {sys.argv[3]}: {status}") + sys.exit(1) + with open(sys.argv[2], "w", encoding="utf-8", newline="") as f: + f.write(result) + print(f"{sys.argv[3]}: {status} ({len(content)} -> {len(result)} bytes)") + + +if __name__ == "__main__": + main()