Add IMAP webmail tag, admin/opensource subpage tiers, store/cart, htmleditor, uploads

New __imap*/__smtpsend/__eval Jet tags give templates real IMAP
(list/message/flag/delete/move/search/folders/accounts) and SMTP-send
capability, backing the new /email webmail client (email.jet/
email.menu, assets/images/email.js + newemail.js) and its
mainpagetemplate.jet-based page shell. Most of this tag family's
source was already being built into the live site (Tag.cpp already
registered __imap/__imaplist/__imapmessage/__imapflag/__imapdelete/
__eval) but had never actually been committed; this also registers
and commits the remaining five (__imapmove/__imapfolders/
__imapsearch/__smtpsend/__imapaccounts).

admin.page/admin.menu gain a real subpage tier (admin-email,
admin-newemail, admin-invoices, admin-products, admin-users) replacing
several previously top-level pages, mirrored by a similar
opensource.page subpage tier (opensource-overview/asm1802/barantmail/
jet/serverframework/synthmax) - old top-level pages retired via git
rename (asm1802.page, jet.page, serverframework.page,
admin_invoices.page); barantmail.page is dropped outright, folded into
opensource-barantmail.subpage.

New storefront (store.page, store-createstore.subpage,
barantstore.page) and cart flow (cart.jet, updatecartqty.proc,
removecartitem.proc, editproduct.page/selectproduct.proc/
update_product.proc for inventory maintenance against the barant
database).

New document upload/download manager (documents.jet, documents_api.jet,
documents_upload.proc, documents_delete.proc,
assets/images/documents.js + documents-page.js) with a persistent
floating panel, and a rich-text page editor (htmleditor.jet/.page,
createhtmleditor.proc, savehtmleditor.proc, assets/images/htmleditor.js
+ its icon/logo assets).

SESSIONCTL.tag: login now redirects back to the originally-requested
page instead of always landing on /account ($[start:0] vs $[start]
variable-name mismatch).

main.css/menu.bar/invoices.menu/jet_overview.section/login.menu:
styling and navigation updates for the above; new site assets (fonts,
images, audio) referenced by them.

jetc/build-assets.sh, list_jet_files.sh, lowercase_jet.py,
strip_fragments.py: small dev tooling used while building the above.
This commit is contained in:
Brad Arant committed 2026-09-06 04:14:00 +00:00
1 parent b0cadc3f08
commit c1491b044b
115 files changed
+6169 -132

No files matched your search

+2
View File
@@ -35,3 +35,5 @@ jetc/libCoreUtils_pic.a
jetc/barant-site/jet/vst.jet jetc/barant-site/jet/vst.jet
jetc/barant-site/jet/modular1.jet jetc/barant-site/jet/modular1.jet
jetc/barant-site/jet/modular1_patches.jet jetc/barant-site/jet/modular1_patches.jet
jetc/**/*.jtc
jetc/__pycache__/
+20
View File
@@ -11,6 +11,11 @@
#include "__imapmessage.h" #include "__imapmessage.h"
#include "__imapflag.h" #include "__imapflag.h"
#include "__imapdelete.h" #include "__imapdelete.h"
#include "__imapmove.h"
#include "__imapfolders.h"
#include "__imapsearch.h"
#include "__smtpsend.h"
#include "__imapaccounts.h"
#include "__whilerow.h" #include "__whilerow.h"
#include "__comment.h" #include "__comment.h"
#include "__eval.h" #include "__eval.h"
@@ -472,6 +477,21 @@ namespace jet {
} else if(ifTagName(in, "imapdelete")) { } else if(ifTagName(in, "imapdelete")) {
__imapdelete _imapdelete(in, out, global, this, local); __imapdelete _imapdelete(in, out, global, this, local);
continue; 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")) { } else if(ifTagName(in, "whilerow")) {
__whilerow _whilerow(in, out, global, this, local); __whilerow _whilerow(in, out, global, this, local);
continue; continue;
+43
View File
@@ -0,0 +1,43 @@
#include "__eval.h"
#include "Exception.h"
namespace jet {
// <eval> 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 <page> definition depends on it
// throughout - capturing a tag's own invocation content into a named
// variable (<eval name="_BODY"><container></eval>, often with
// scope="parent" so a nested custom tag can write back into its
// caller's scope: <eval name="AUTHOR" scope="parent">...), and also
// using it bare (no name=) to render an <include>'d file's content
// inline (<eval><include file="...page"></eval>). Without this tag,
// all of that content was silently falling through as undefined-tag
// literal text, which is why a real, unmodified SESSIONCTL.tag <page>
// never produced its actual rendered body.
//
// <eval name="X" [scope="local|parent|global"]>content</eval> - same
// capture semantics as <set name="X">content</set> (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 <set> does). Reused directly rather than
// reimplemented, since it's the identical contract under a different
// tag name.
//
// <eval>content</eval> (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);
}
}
}
+21
View File
@@ -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
+1100
View File
File diff suppressed because it is too large. Load diff
+112
View File
@@ -0,0 +1,112 @@
#ifndef ____imap_h__
# define ____imap_h__
# include "Tag.h"
# include "ZString.h"
# include "MString.h"
# include <cstdint>
# include <openssl/ssl.h>
# include <string>
# include <vector>
namespace jet {
///
/// <imap config="./email.cfg" user="..." password="..." sessionid="">
///
/// 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 <sql> finds its
/// <database> 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 <sql>.
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<std::string> 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<std::string> findFlagsField(const std::string &fetchLine);
static long findSizeField(const std::string &fetchLine);
// Extracts the literal content immediately following a
// "BODY[...]" (or "BODY[...]<n>") 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<std::string> parseSummaryLines(const std::vector<std::string> &lines);
};
}
#endif
+139
View File
@@ -0,0 +1,139 @@
#include "__imapaccounts.h"
#include "Base64.h"
#include "Exception.h"
#include <cstdio>
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<std::pair<std::string, std::string>> __imapaccounts::decodeList(const std::string &encodedRaw) {
std::vector<std::pair<std::string, std::string>> entries;
coreutils::Base64 base64;
// The list value round-trips through <eval>/<set> 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<std::pair<std::string, std::string>> &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);
}
}
}
+59
View File
@@ -0,0 +1,59 @@
#ifndef ____imapaccounts_h__
# define ____imapaccounts_h__
# include "Tag.h"
# include "ZString.h"
# include "MString.h"
# include <string>
# include <utility>
# include <vector>
namespace jet {
///
/// <imapaccounts action="add|remove|getpassword|firstuser|list"
/// list="$[@_imap_accounts]" user="..." password="...">
///
/// 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 <eval name="_imap_accounts"> itself and
/// <set>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<std::pair<std::string, std::string>> decodeList(const std::string &encoded);
static std::string encodeList(const std::vector<std::pair<std::string, std::string>> &entries);
static std::string jsonEscape(const std::string &s);
};
}
#endif
+25
View File
@@ -0,0 +1,25 @@
#include "__imapdelete.h"
#include "Exception.h"
#include "__imap.h"
#include <cstdlib>
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);
}
}
+25
View File
@@ -0,0 +1,25 @@
#ifndef ____imapdelete_h__
# define ____imapdelete_h__
# include "Tag.h"
namespace jet {
///
/// <imapdelete sessionid="" folder="INBOX" uid="123">
///
/// 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
+32
View File
@@ -0,0 +1,32 @@
#include "__imapflag.h"
#include "Exception.h"
#include "__imap.h"
#include <cstdlib>
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);
}
}
+26
View File
@@ -0,0 +1,26 @@
#ifndef ____imapflag_h__
# define ____imapflag_h__
# include "Tag.h"
namespace jet {
///
/// <imapflag sessionid="" folder="INBOX" uid="123" flag="\Seen" set="yes">
///
/// 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
+16
View File
@@ -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();
}
}
+27
View File
@@ -0,0 +1,27 @@
#ifndef ____imapfolders_h__
# define ____imapfolders_h__
# include "Tag.h"
namespace jet {
///
/// <imapfolders sessionid="">
///
/// 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
+26
View File
@@ -0,0 +1,26 @@
#include "__imaplist.h"
#include "Exception.h"
#include "__imap.h"
#include <cstdlib>
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);
}
}
+30
View File
@@ -0,0 +1,30 @@
#ifndef ____imaplist_h__
# define ____imaplist_h__
# include "Tag.h"
namespace jet {
///
/// <imaplist sessionid="" folder="INBOX" limit="50">
///
/// 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
+25
View File
@@ -0,0 +1,25 @@
#include "__imapmessage.h"
#include "Exception.h"
#include "__imap.h"
#include <cstdlib>
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);
}
}
+25
View File
@@ -0,0 +1,25 @@
#ifndef ____imapmessage_h__
# define ____imapmessage_h__
# include "Tag.h"
namespace jet {
///
/// <imapmessage sessionid="" folder="INBOX" uid="123">
///
/// 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
+29
View File
@@ -0,0 +1,29 @@
#include "__imapmove.h"
#include "Exception.h"
#include "__imap.h"
#include <cstdlib>
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);
}
}
+29
View File
@@ -0,0 +1,29 @@
#ifndef ____imapmove_h__
# define ____imapmove_h__
# include "Tag.h"
namespace jet {
///
/// <imapmove sessionid="" folder="INBOX" uid="123" destfolder="Trash">
///
/// 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
+22
View File
@@ -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);
}
}
+27
View File
@@ -0,0 +1,27 @@
#ifndef ____imapsearch_h__
# define ____imapsearch_h__
# include "Tag.h"
namespace jet {
///
/// <imapsearch sessionid="" folder="INBOX" query="text">
///
/// Self-closing tag - writes a JSON array of message summaries
/// (same shape as <imaplist>) 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
+220
View File
@@ -0,0 +1,220 @@
#include "__smtpsend.h"
#include "Exception.h"
#include "JFile.h"
#include "Base64.h"
#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <ctime>
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}"));
}
}
+69
View File
@@ -0,0 +1,69 @@
#ifndef ____smtpsend_h__
# define ____smtpsend_h__
# include "Tag.h"
# include "ZString.h"
# include "MString.h"
# include <openssl/ssl.h>
# include <string>
namespace jet {
///
/// <smtpsend config="./email.cfg" user="..." password="..."
/// to="..." subject="..." html="...">
///
/// 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 <imap> (so the message goes out authenticated as the
/// actual connected mailbox, not the site's own transactional-mail
/// identity - see barant.tag's separate <email> 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
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -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 = '<div id="documents-empty">No documents yet - upload your first one above.</div>';
return;
}
var html = '<table id="documents-list"><tr><th>Name</th><th>Uploaded</th><th></th><th></th></tr>';
for (var i = 0; i < res.documents.length; i++) {
var doc = res.documents[i];
html += '<tr>' +
'<td>' + escapeHtml(doc.name) + '</td>' +
'<td>' + escapeHtml(doc.created) + '</td>' +
'<td><span class="documents-action" data-action="download" data-id="' + doc.id + '" data-name="' + escapeHtml(doc.name) + '">Download</span></td>' +
'<td><span class="documents-action" data-action="delete" data-id="' + doc.id + '">Delete</span></td>' +
'</tr>';
}
html += '</table>';
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 = '<div id="documents-empty">Could not load your documents.</div>';
});
}
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();
})();
+178
View File
@@ -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 = '<div id="filemgr-empty">No transfers yet.</div>';
}
var UPLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V6M5 12l7-7 7 7"/></svg>';
var DOWNLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v13M5 12l7 7 7-7"/></svg>';
function addTransfer(name, direction) {
var id = 'xfer' + (nextId++);
removeEmptyPlaceholder();
var el = document.createElement('div');
el.className = 'filemgr-item';
el.innerHTML =
'<div class="filemgr-item-row">' +
'<span class="filemgr-item-icon">' + (direction === 'upload' ? UPLOAD_ICON : DOWNLOAD_ICON) + '</span>' +
'<span class="filemgr-item-name">' + escapeHtml(name) + '</span>' +
'<span class="filemgr-item-status">0%</span>' +
'</div>' +
'<div class="filemgr-progress-track"><div class="filemgr-progress-fill"></div></div>';
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 <a href> 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 <a download> 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();
});
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

+778
View File
@@ -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 <button>'s own
// text color (the site's gold theme, see .icon-btn in email.page's
// own <style> block), no separate image assets or color variants to
// keep in sync with the theme.
var ICONS = {
search:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<circle cx="11" cy="11" r="7"/>' +
'<path d="M21 21l-4.35-4.35"/>' +
'</svg>',
compose:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M4 6h13a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/>' +
'<path d="M2.5 7.5 12 14l9.5-6.5"/>' +
'<path d="M18.5 3.5v5M16 6h5"/>' +
'</svg>',
refresh:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M20 11a8 8 0 0 0-14.6-4.4M4 4v5h5"/>' +
'<path d="M4 13a8 8 0 0 0 14.6 4.4M20 20v-5h-5"/>' +
'</svg>',
disconnect:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>' +
'<path d="M16 17l5-5-5-5"/>' +
'<path d="M21 12H9"/>' +
'</svg>',
flag:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M5 3v18"/>' +
'<path d="M5 4h12l-3 4.5L17 13H5"/>' +
'</svg>',
trash:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M4 7h16"/>' +
'<path d="M9 7V4h6v3"/>' +
'<path d="M6 7l1 13a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-13"/>' +
'<path d="M10 11v6M14 11v6"/>' +
'</svg>'
};
// Matches the exact, case-sensitive names the mailbox itself reports
// via IMAP LIST (confirmed live: "Inbox", "Spam", "Trash" - only
// INBOX is specially case-insensitive per RFC 3501, the other two
// are not, so this has to be exact) - see email.menu's own folder
// links, which pass these same three names to emailSelectFolder().
var currentFolder = 'Inbox';
var currentUid = null;
// null = showing the normal folder listing; a non-empty string =
// showing search results instead (see performSearch()/clearSearch()
// and loadMessageList()'s own branch on this).
var currentSearchQuery = null;
// The array last handed to renderMessageList() - kept so openMessage()
// can flip a single message's .seen flag locally and recompute the
// toolbar's unread/total count without a round trip.
var currentMessages = [];
// email.menu (the site's own left-side #panelmenu, a static include
// living outside #email-app/#content entirely - see mainpagetemplate
// .jet's own panelmenu wiring) renders its folder links against this
// global rather than anything scoped to this closure, since it has
// no other way to reach into a same-page IMAP client without a full
// page navigation (which the rest of this client deliberately avoids
// for every other interaction too - see the module doc comment on
// post() below... [see loadMessageList/openMessage]). Guarded with
// `window.emailSelectFolder &&` at each call site in email.menu so a
// click before email.js has loaded (or before a mailbox is even
// connected) is a harmless no-op instead of a broken link.
window.emailSelectFolder = function(folder) {
if (!document.getElementById('email-client') || (folder === currentFolder && !currentSearchQuery))
return;
currentFolder = folder;
currentUid = null;
// A folder click always means "show me this folder, plain" - drops
// out of search mode even if the folder didn't actually change,
// which is exactly how a user would expect to back out of a
// search result view.
currentSearchQuery = null;
var searchInput = document.getElementById('email-search-input');
if (searchInput) searchInput.value = '';
syncActiveFolder();
var pane = document.getElementById('email-reading-pane');
if (pane) pane.innerHTML = '<div id="email-empty-state">Select a message to read it.</div>';
loadMessageList();
};
function syncActiveFolder() {
var links = document.querySelectorAll('.email-folder-link');
for (var i = 0; i < links.length; i++)
links[i].classList.toggle('email-folder-selected', links[i].getAttribute('data-folder') === currentFolder);
}
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(); });
}
// Matches spa.js's own delay() exactly - a separate closure here
// since this file has no dependency on spa.js at all, not worth
// sharing just for this.
function delay(ms) {
return new Promise(function(resolve) { setTimeout(resolve, ms); });
}
function showConnectForm(errorText) {
app.innerHTML =
'<div id="email-connect-form">' +
'<h3>Connect to Mailbox</h3>' +
'<input type="text" id="email-user-input" placeholder="you@barant.com" autocomplete="username">' +
'<input type="password" id="email-password-input" placeholder="Password" autocomplete="current-password">' +
'<button type="button" id="email-connect-btn">Connect</button>' +
'<div id="email-connect-error">' + (errorText ? escapeHtml(errorText) : '') + '</div>' +
'</div>';
wireConnectForm();
}
function showClientView(userEmail) {
app.innerHTML =
'<div id="email-client">' +
'<div id="email-toolbar">' +
'<span id="email-connected-as">' + escapeHtml(userEmail) + '</span>' +
'<span id="email-list-count"></span>' +
'<span id="email-toolbar-actions">' +
'<span id="email-search-box">' +
'<input type="text" id="email-search-input" placeholder="Search mail&hellip;">' +
'<button type="button" id="email-search-btn" class="icon-btn" title="Search">' + ICONS.search + '</button>' +
'</span>' +
'<button type="button" id="email-compose-btn" class="icon-btn" title="New Mail">' + ICONS.compose + '</button>' +
'<button type="button" id="email-refresh-btn" class="icon-btn" title="Refresh">' + ICONS.refresh + '</button>' +
'<button type="button" id="email-disconnect-btn" class="icon-btn" title="Disconnect">' + ICONS.disconnect + '</button>' +
'</span>' +
'</div>' +
'<div id="email-layout">' +
'<div id="email-list"></div>' +
'<div id="email-reading-pane"><div id="email-empty-state">Select a message to read it.</div></div>' +
'</div>' +
'</div>';
wireClientView();
wireAddAccountForm();
syncActiveFolder();
loadMessageList();
loadAccounts();
}
function wireConnectForm() {
var btn = document.getElementById('email-connect-btn');
var userInput = document.getElementById('email-user-input');
var passInput = document.getElementById('email-password-input');
var errorDiv = document.getElementById('email-connect-error');
function submit() {
errorDiv.textContent = '';
btn.disabled = true;
post('/email/connect', { user: userInput.value, password: passInput.value })
.then(function(res) {
btn.disabled = false;
if (res.ok) {
showClientView(userInput.value);
} else {
errorDiv.textContent = res.error || 'Could not connect.';
}
})
.catch(function() {
btn.disabled = false;
errorDiv.textContent = 'Connection request failed.';
});
}
btn.addEventListener('click', submit);
passInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submit();
});
}
function wireClientView() {
// Navigates via a synthetic <a> click rather than
// window.location.href so spa.js's own document-level click
// listener intercepts it and does its normal fade transition,
// exactly as if a real link had been clicked.
document.getElementById('email-compose-btn').addEventListener('click', function() {
var a = document.createElement('a');
a.href = '/admin/newemail';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
document.getElementById('email-refresh-btn').addEventListener('click', function() {
loadMessageList();
});
document.getElementById('email-disconnect-btn').addEventListener('click', function() {
// Disconnecting only ever drops the ACTIVE account now (see
// /email/disconnect's own doc comment in email.jet) - if another
// already-connected account remains, res.active names it and the
// view just switches there instead of falling back to the
// connect form.
post('/email/disconnect', {}).then(function(res) {
if (res.active) {
resetViewForActiveAccount(res.active);
} else {
currentFolder = 'Inbox';
currentUid = null;
syncActiveFolder();
showConnectForm();
}
});
});
// Matches sender, subject, or body content in whatever folder is
// currently open (see __imap.cpp's searchMessages()) - an empty
// query is treated as "leave search mode" rather than sent to the
// server as a real search, since an empty needle would otherwise
// just match everything anyway (see __IMAP_SEARCH.cpp's own
// containsCI()) - clearer to just fall back to the plain listing.
function runSearch() {
var input = document.getElementById('email-search-input');
var query = input.value.trim();
currentSearchQuery = query === '' ? null : query;
currentUid = null;
document.getElementById('email-reading-pane').innerHTML =
'<div id="email-empty-state">Select a message to read it.</div>';
loadMessageList();
}
document.getElementById('email-search-btn').addEventListener('click', runSearch);
document.getElementById('email-search-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter')
runSearch();
});
}
function updateListCount(messages) {
var countEl = document.getElementById('email-list-count');
if (!countEl)
return;
if (!messages.length) {
countEl.textContent = '';
return;
}
var unread = 0;
for (var i = 0; i < messages.length; i++) {
if (!messages[i].seen)
unread++;
}
countEl.textContent = '(' + unread + '/' + messages.length + ')';
}
function renderMessageList(messages) {
var list = document.getElementById('email-list');
currentMessages = messages;
updateListCount(messages);
if (!messages.length) {
list.innerHTML = '<div id="email-empty-state">No messages.</div>';
return;
}
var html = '';
for (var i = 0; i < messages.length; i++) {
var m = messages[i];
html += '<div class="email-row' + (m.seen ? '' : ' unread') +
(m.uid === currentUid ? ' selected' : '') + '" data-uid="' + m.uid + '">' +
'<div class="email-row-info">' +
'<div class="email-from">' + escapeHtml(m.from) + '</div>' +
'<div class="email-subject">' + escapeHtml(m.subject || '(no subject)') + '</div>' +
'<div class="email-date">' + escapeHtml(m.date) + '</div>' +
'</div>' +
'<button type="button" class="icon-btn email-row-trash-btn" data-uid="' + m.uid + '" title="Move to Trash">' + ICONS.trash + '</button>' +
'</div>';
}
list.innerHTML = html;
var rows = list.querySelectorAll('.email-row');
for (var j = 0; j < rows.length; j++) {
rows[j].addEventListener('click', function() {
openMessage(parseInt(this.getAttribute('data-uid'), 10));
});
}
// Each row's own trash button - stopPropagation so it doesn't also
// trigger the row's own click-to-open handler just above.
var trashBtns = list.querySelectorAll('.email-row-trash-btn');
for (var k = 0; k < trashBtns.length; k++) {
trashBtns[k].addEventListener('click', function(e) {
e.stopPropagation();
trashMessage(parseInt(this.getAttribute('data-uid'), 10));
});
}
// Whatever caused this particular render (initial connect, a
// folder switch, the refresh button with nothing open) left
// currentUid null - open the top message rather than leave the
// reading pane sitting empty. trashMessage() deliberately sets
// currentUid to the row it's already decided to select BEFORE
// reloading the list (see its own comment), specifically so this
// check sees a real, already-correct uid and stays out of the way
// instead of racing it to open something different.
if (currentUid === null)
openMessage(messages[0].uid);
}
// Moves a message into the Trash folder (server-side: COPY there,
// then \Deleted+EXPUNGE from the original folder - see __imap.cpp's
// moveMessage()) rather than permanently removing it, the same
// "delete means trash it" convention the reading pane's own Delete
// button (a real, permanent expunge) deliberately does NOT follow -
// that one stays as-is; this is a separate, softer action reachable
// straight from the list.
// Total time the .email-row-removing CSS animation takes to finish
// (see email.page's own <style> block) - the opacity fade runs with
// no delay for FADE_MS, then the height/padding/margin/border
// collapse (what actually slides the rows below it up) runs for
// COLLAPSE_MS starting COLLAPSE_DELAY_MS in. Kept here as the single
// source of truth for how long trashMessage() waits before swapping
// in the server's real, post-trash list - duplicated as literal
// numbers in the CSS transition properties themselves, since CSS has
// no way to reference a JS constant, so keep those two in sync by
// hand if either ever changes.
var REMOVE_FADE_MS = 200;
var REMOVE_COLLAPSE_DELAY_MS = 200;
var REMOVE_COLLAPSE_MS = 220;
// Plays the dissolve-then-collapse removal animation on one row in
// place, resolving once it's fully finished (safe to call with a
// null row - resolves immediately, for when the trashed uid's row
// isn't actually in the currently-rendered list for some reason).
function animateRowRemoval(row) {
if (!row)
return Promise.resolve();
var height = row.getBoundingClientRect().height;
row.style.height = height + 'px';
// Forces the browser to register the explicit pixel height above
// as a real, already-applied style before the next line changes
// it again - without this the two height changes collapse into a
// single layout pass and no transition plays at all.
row.offsetHeight;
row.classList.add('email-row-removing');
return new Promise(function(resolve) {
setTimeout(resolve, REMOVE_FADE_MS + REMOVE_COLLAPSE_DELAY_MS + REMOVE_COLLAPSE_MS);
});
}
// Only the trashed row itself animates - every other row just stays
// exactly where it is and shifts up as an ordinary side effect of
// the trashed row's own height collapsing to 0 (real document-flow
// reflow, not a separate animation applied to them). The list is
// still reloaded from the server afterward (flags/counts could have
// changed for reasons unrelated to this one trash action), but with
// loadMessageList(true) - skipping ITS OWN whole-list fade, since by
// the time it swaps in fresh markup the trashed row has already
// visually shrunk to nothing and every other row is already sitting
// in its final position; fading the whole list on top of that would
// just be a second, redundant flash.
function trashMessage(uid) {
var rows = document.querySelectorAll('.email-row');
var index = -1;
var trashedRow = null;
for (var i = 0; i < rows.length; i++) {
if (parseInt(rows[i].getAttribute('data-uid'), 10) === uid) {
index = i;
trashedRow = rows[i];
break;
}
}
var nextUid = null;
if (index !== -1) {
if (index + 1 < rows.length)
nextUid = parseInt(rows[index + 1].getAttribute('data-uid'), 10);
else if (index - 1 >= 0)
nextUid = parseInt(rows[index - 1].getAttribute('data-uid'), 10);
}
Promise.all([
animateRowRemoval(trashedRow),
post('/email/trash', { folder: currentFolder, uid: uid })
]).then(function() {
// Set BEFORE the reload, not after - renderMessageList()'s own
// auto-select-first-when-nothing's-selected check runs partway
// through loadMessageList(true) below, and needs to already see
// the real target here (or genuinely null, if the list is about
// to be empty) rather than a stale null that would make it pick
// the top row instead and race this function's own openMessage()
// call just below.
if (currentUid === uid)
currentUid = nextUid;
loadMessageList(true).then(function() {
if (nextUid !== null)
openMessage(nextUid);
else
document.getElementById('email-reading-pane').innerHTML =
'<div id="email-empty-state">Select a message to read it.</div>';
});
});
}
// Same fade-out/fetch-concurrently/fade-in shape as openMessage() and
// spa.js's own navigate() - every list refresh (folder switch,
// refresh button) gets the same smooth transition instead of a flash
// of "Loading..." text followed by an abrupt content swap. Pass
// skipFade=true to skip that whole-list transition and just quietly
// re-render in place - used only right after trashMessage()'s own
// per-row removal animation has already provided the visual
// transition, so this reload doesn't also blink the entire list.
function loadMessageList(skipFade) {
var list = document.getElementById('email-list');
if (!list)
return Promise.resolve();
if (!skipFade)
list.classList.add('email-fading');
// Search results use the identical JSON shape as a normal list
// (see __imap.cpp's shared parseSummaryLines()), so this is the
// only place that needs to know search mode even exists -
// renderMessageList() below has no idea which one produced what
// it's rendering.
var request = currentSearchQuery
? post('/email/search', { folder: currentFolder, query: currentSearchQuery })
: post('/email/list', { folder: currentFolder });
return Promise.all([
request,
skipFade ? true : delay(180)
])
.then(function(results) {
var res = results[0];
if (!res.ok && res.error)
list.innerHTML = '<div id="email-empty-state">' + escapeHtml(res.error) + '</div>';
else
renderMessageList(Array.isArray(res) ? res : []);
// Every place that reloads the list (connect, folder switch,
// refresh, post-trash) is a place the unseen counts next to
// each folder name could plausibly have changed too - one
// hook here covers all of them rather than needing a separate
// call at each individual call site.
loadFolderCounts();
if (!skipFade)
list.classList.remove('email-fading');
})
.catch(function() {
list.innerHTML = '<div id="email-empty-state">Could not load messages.</div>';
if (!skipFade)
list.classList.remove('email-fading');
});
}
// Populates the unread-count badge next to each folder name in
// email.menu's own static markup (a real DOM lookup by
// data-folder-count, not anything rendered by this file - email.menu
// is a separate include living in #panelmenu, outside #content
// entirely). Uses IMAP STATUS server-side (see __imap.cpp's
// folderCounts()), not a real FETCH, so this is cheap enough to call
// on every list reload without it being a heavy extra round trip.
// Silently does nothing on error - a stale/missing count badge is a
// cosmetic problem, not worth surfacing as a user-visible failure.
function loadFolderCounts() {
post('/email/folders', {}).then(function(res) {
if (res.error)
return;
var folders = ['Inbox', 'Spam', 'Trash'];
for (var i = 0; i < folders.length; i++) {
var f = folders[i];
var span = document.querySelector('.email-folder-count[data-folder-count="' + f + '"]');
if (span && res[f])
span.textContent = res[f].messages > 0 ? (res[f].unseen + '/' + res[f].messages) : '';
}
}).catch(function() {});
}
function loadAccounts() {
post('/email/accounts', {}).then(function(res) {
var list = document.getElementById('email-account-list');
if (!list || !res.accounts)
return;
var html = '';
for (var i = 0; i < res.accounts.length; i++) {
var user = res.accounts[i];
html += '<a href="javascript:void(0);" class="email-account-link' +
(user === res.active ? ' email-account-selected' : '') + '" data-user="' +
escapeHtml(user) + '">' + escapeHtml(user) + '</a>';
}
list.innerHTML = html;
var links = list.querySelectorAll('.email-account-link');
for (var j = 0; j < links.length; j++) {
links[j].addEventListener('click', function() {
var user = this.getAttribute('data-user');
if (!this.classList.contains('email-account-selected'))
switchAccount(user);
});
}
}).catch(function() {});
}
// Shared by switchAccount() and the add-account form's own success
// path (which is already connected/active server-side by the time
// /email/connect returns ok - no reason to also round-trip through
// switchaccount right after) - resets the view to a fresh Inbox for
// whichever account just became active, same as a brand new connect.
function resetViewForActiveAccount(user) {
currentFolder = 'Inbox';
currentUid = null;
currentSearchQuery = null;
var searchInput = document.getElementById('email-search-input');
if (searchInput) searchInput.value = '';
var connectedAs = document.getElementById('email-connected-as');
if (connectedAs) connectedAs.textContent = user;
syncActiveFolder();
var pane = document.getElementById('email-reading-pane');
if (pane) pane.innerHTML = '<div id="email-empty-state">Select a message to read it.</div>';
loadMessageList();
loadAccounts();
}
function switchAccount(user) {
post('/email/switchaccount', { user: user }).then(function(res) {
if (res.ok)
resetViewForActiveAccount(user);
}).catch(function() {});
}
window.emailShowAddAccount = function() {
var form = document.getElementById('email-add-account-form');
var link = document.getElementById('email-add-account-link');
if (!form)
return;
form.style.display = 'flex';
if (link) link.style.display = 'none';
var userInput = document.getElementById('email-add-account-user');
if (userInput) userInput.focus();
};
function wireAddAccountForm() {
var btn = document.getElementById('email-add-account-btn');
var userInput = document.getElementById('email-add-account-user');
var passInput = document.getElementById('email-add-account-password');
var errorDiv = document.getElementById('email-add-account-error');
if (!btn)
return;
function submit() {
errorDiv.textContent = '';
btn.disabled = true;
post('/email/connect', { user: userInput.value, password: passInput.value })
.then(function(res) {
btn.disabled = false;
if (!res.ok) {
errorDiv.textContent = res.error || 'Could not connect.';
return;
}
var newUser = userInput.value;
userInput.value = '';
passInput.value = '';
document.getElementById('email-add-account-form').style.display = 'none';
document.getElementById('email-add-account-link').style.display = 'block';
resetViewForActiveAccount(newUser);
})
.catch(function() {
btn.disabled = false;
errorDiv.textContent = 'Connection request failed.';
});
}
btn.addEventListener('click', submit);
passInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submit();
});
}
function renderMessage(msg) {
var pane = document.getElementById('email-reading-pane');
pane.innerHTML =
'<div class="email-header">' +
'<div><b>From:</b> ' + escapeHtml(msg.from) + '</div>' +
'<div><b>To:</b> ' + escapeHtml(msg.to) + '</div>' +
'<div><b>Subject:</b> ' + escapeHtml(msg.subject || '(no subject)') + '</div>' +
'<div><b>Date:</b> ' + escapeHtml(msg.date) + '</div>' +
'</div>' +
'<div class="email-body-container"></div>' +
'<div class="email-actions">' +
'<button type="button" id="email-flag-btn" class="icon-btn' + (msg.flagged ? ' icon-btn-active' : '') +
'" title="' + (msg.flagged ? 'Unflag' : 'Flag') + '">' + ICONS.flag + '</button>' +
'<button type="button" id="email-delete-btn" class="icon-btn" title="Delete">' + ICONS.trash + '</button>' +
'</div>';
// HTML-formatted mail renders in a sandboxed iframe, never via
// innerHTML into this page directly - the body content is whatever
// a third party sent, and this page runs as the logged-in user
// with real session cookies. `allow-same-origin` alone (no
// `allow-scripts`, no `allow-forms`, no `allow-popups`, no
// `allow-top-navigation`) still blocks every dangerous thing
// untrusted HTML could do - scripts simply do not run without
// allow-scripts, full stop, regardless of same-origin access - it
// ONLY additionally lets THIS PAGE read the iframe's own rendered
// content height back out (contentDocument), which the very first,
// fully-opaque `sandbox=""` version couldn't do at all. That's
// needed to size the iframe to its actual content below, rather
// than a fixed height with its own internal scrollbar sitting
// inside the reading pane's own scrollbar - two nested scrollbars
// for one piece of content, confusing regardless of how tall the
// fixed box happened to be. Building it via createElement + the
// .srcdoc PROPERTY (not a string-concatenated HTML attribute)
// sidesteps any need to hand-escape the content for attribute-
// safety - the DOM API does that correctly on its own. Plain-text
// mail keeps using .textContent, same reasoning.
var bodyContainer = pane.querySelector('.email-body-container');
if (msg.isHtml) {
var frame = document.createElement('iframe');
frame.className = 'email-body-frame';
frame.setAttribute('sandbox', 'allow-same-origin');
frame.setAttribute('scrolling', 'no');
frame.addEventListener('load', function() {
try {
var doc = frame.contentDocument;
if (doc && doc.documentElement)
frame.style.height = Math.max(doc.documentElement.scrollHeight, doc.body.scrollHeight) + 'px';
} catch (e) {
// Left at its CSS fallback height - still shows the content,
// just with its own scrollbar if that fallback runs short.
}
});
frame.srcdoc = msg.body;
bodyContainer.appendChild(frame);
} else {
var textDiv = document.createElement('div');
textDiv.className = 'email-body';
textDiv.textContent = msg.body;
bodyContainer.appendChild(textDiv);
}
document.getElementById('email-flag-btn').addEventListener('click', function() {
post('/email/flag', {
folder: currentFolder,
uid: msg.uid,
flag: '\\Flagged',
set: msg.flagged ? 'no' : 'yes'
}).then(function() {
loadMessageList();
openMessage(msg.uid);
});
});
document.getElementById('email-delete-btn').addEventListener('click', function() {
if (!window.confirm('Delete this message?')) return;
post('/email/delete', { folder: currentFolder, uid: msg.uid }).then(function() {
currentUid = null;
document.getElementById('email-reading-pane').innerHTML =
'<div id="email-empty-state">Select a message to read it.</div>';
loadMessageList();
});
});
}
// Opening a message is the reading pane's own concern - it fetches
// and renders itself via /email/message and nothing else. It used to
// also call loadMessageList() afterward (re-fetching and
// re-rendering the ENTIRE list, every single time a message was
// opened) just to pick up the \Seen flag the server already set as a
// side effect of fetching the body - that's a locally-known update
// (we know which uid was just opened), applied straight to the
// already-rendered row below instead of paying for a second full
// round trip and list re-render on every click.
function openMessage(uid) {
currentUid = uid;
var pane = document.getElementById('email-reading-pane');
var rows = document.querySelectorAll('.email-row');
var wasUnread = false;
for (var i = 0; i < rows.length; i++) {
var isTarget = parseInt(rows[i].getAttribute('data-uid'), 10) === uid;
rows[i].classList.toggle('selected', isTarget);
if (isTarget) {
wasUnread = rows[i].classList.contains('unread');
// Marked read the instant it's selected, not after the fetch
// resolves - fetchMessage() marks it \Seen server-side anyway
// (BODY[] rather than BODY.PEEK[]), so there's nothing
// optimistic about this beyond removing the visual lag.
rows[i].classList.remove('unread');
}
}
if (wasUnread) {
for (var k = 0; k < currentMessages.length; k++) {
if (currentMessages[k].uid === uid)
currentMessages[k].seen = true;
}
updateListCount(currentMessages);
loadFolderCounts();
}
// Same fade-out/fetch-concurrently/fade-in shape as spa.js's own
// navigate() - a minimum-duration fade (via delay()) runs alongside
// the fetch rather than after it, so a fast response still shows
// the full transition instead of a flash, and a slow one doesn't
// wait any longer than the fetch itself already takes.
pane.classList.add('email-fading');
Promise.all([
post('/email/message', { folder: currentFolder, uid: uid }),
delay(180)
])
.then(function(results) {
var res = results[0];
if (!res.ok && res.error) {
pane.innerHTML = '<div id="email-empty-state">' + escapeHtml(res.error) + '</div>';
} else {
renderMessage(res);
}
pane.classList.remove('email-fading');
})
.catch(function() {
pane.innerHTML = '<div id="email-empty-state">Could not load message.</div>';
pane.classList.remove('email-fading');
});
}
// Up/Down moves through the message list and opens whatever it
// lands on - registered once, unconditionally, at load time (same
// always-on-but-internally-guarded shape as spa.js's own document-
// level click listener) rather than added/removed each time the
// client view is (re)shown, so reconnecting never stacks up
// duplicate handlers. Bails out immediately whenever the client
// view isn't actually showing (the connect form has no .email-row
// elements at all) or the keystroke is really meant for a form
// field (a future compose/search input, say) rather than list
// navigation.
document.addEventListener('keydown', function(e) {
var active = document.activeElement;
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA'))
return;
// Delete trashes whichever message is currently open, the same
// action (and the same dissolve-and-slide-up animation) as
// clicking that row's own trash button - see trashMessage().
if (e.key === 'Delete') {
if (!document.getElementById('email-client') || currentUid === null)
return;
e.preventDefault();
trashMessage(currentUid);
return;
}
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp')
return;
var rows = document.querySelectorAll('.email-row');
if (!rows.length)
return;
e.preventDefault();
var index = -1;
for (var i = 0; i < rows.length; i++) {
if (parseInt(rows[i].getAttribute('data-uid'), 10) === currentUid) {
index = i;
break;
}
}
if (e.key === 'ArrowDown')
index = (index === -1) ? 0 : Math.min(index + 1, rows.length - 1);
else
index = (index === -1) ? 0 : Math.max(index - 1, 0);
rows[index].scrollIntoView({ block: 'nearest' });
openMessage(parseInt(rows[index].getAttribute('data-uid'), 10));
});
if (document.getElementById('email-connect-form')) {
wireConnectForm();
} else if (document.getElementById('email-client')) {
wireClientView();
wireAddAccountForm();
syncActiveFolder();
loadMessageList();
loadAccounts();
}
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

@@ -0,0 +1,678 @@
function b64encode(str) {
return btoa(unescape(encodeURIComponent(str)));
}
function submitNewLetter(form) {
var name = document.getElementById('filename_display').value;
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
alert('File name may only contain letters, numbers, underscores and hyphens.');
return false;
}
form.filename.value = name;
return true;
}
var mainpage;
var control;
var result;
var mousedownx;
var mousedowny;
var dragHint = "move";
var data;
var showGrid = false;
var snapToGrid = false;
var gridSize = 10;
var selected;
var firstSelected;
var itemparameters;
var grid;
var context;
var draggingHTML;
var window_dragging = false;
const undo = [];
var undoIndex = 0;
var lastRedo;
var mode = 'MOUSEUP';
var textobj;
var lastdragover;
var pendingDragover;
var dropIndicator;
var sequence = 0;
function showDropIndicator(target) {
if(!dropIndicator) {
dropIndicator = document.createElement('div');
dropIndicator.style.position = 'fixed';
dropIndicator.style.height = '4px';
dropIndicator.style.background = '#0088ff';
dropIndicator.style.zIndex = '99999';
dropIndicator.style.pointerEvents = 'none';
document.body.appendChild(dropIndicator);
}
let rect = target.getBoundingClientRect();
dropIndicator.style.left = rect.left + 'px';
dropIndicator.style.width = rect.width + 'px';
dropIndicator.style.top = (rect.top - 2) + 'px';
dropIndicator.style.display = 'block';
}
function hideDropIndicator() {
if(dropIndicator) {
dropIndicator.style.display = 'none';
}
}
function saveUndo() {
undo[undoIndex++] = mainpage.innerHTML;
}
function do_undo() {
if(undoIndex > 0) {
mainpage.innerHTML = undo[--undoIndex];
}
return true;
}
function do_redo() {
return true;
}
function init() {
mainpage = document.getElementById("mainpage");
data = document.getElementById("data");
itemparameters = document.getElementById("itemparameters");
var gridsize = document.getElementById("gridsize");
gridsize.value = gridSize;
grid = document.getElementById("grid");
context = grid.getContext("2d");
drawGrid();
buildToolbar();
document.designMode = 'off';
}
function getLayerMap(element) {
var map = '';
if(element.id != 'mainpage') {
var curelem = element;
while(curelem.id != 'mainpage') {
map = '&lt;' + curelem.nodeName + '&gt;' + map;
curelem = curelem.parentNode;
}
}
document.getElementById('map').innerHTML = map;
}
function jetEscape(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function buildToolbar() {
let tbitems = [
{
"label": "&lt;div>",
"html": "<div id=\"newitem\" contenteditable=\"false\" style=\"width: 300px; height: 200px; background-color: #789;\"></div>"
},
{
"label": "&lt;input>",
"html": "<input id=\"newitem\" type=\"text\">"
},
{
"label": "&lt;button>",
"html": "<button id=\"newitem\" type=\"button\">Label</button>"
},
{
"label": "&lt;password>",
"html": "<input id=\"newitem\" type=\"password\">"
},
{
"label": "&lt;checkbox>",
"html": "<input id=\"newitem\" type=\"checkbox\">"
},
{
"label": "&lt;label>",
"html": "<label style=\"color: #000f;\" id=\"newitem\">Label text</label>"
},
{
"label": "&lt;p>",
"html": "<p style=\"color: #000f;\" id=\"newitem\">Type Here</p>"
},
{
"label": "&lt;hr>",
"html": "<hr id=\"newitem\">"
},
{
"label": "Jet: " + "$" + "[var]",
"html": "<code id=\"newitem\" contenteditable=\"true\" style=\"display:inline-block; background:#eef; border:1px dashed #99c; padding:4px; font-family:monospace; white-space:pre;\">" + jetEscape("$" + "[varname]") + "</code>"
},
{
"label": "Jet: &lt;set&gt;",
"html": "<code id=\"newitem\" contenteditable=\"true\" style=\"display:inline-block; background:#eef; border:1px dashed #99c; padding:4px; font-family:monospace; white-space:pre;\">" + jetEscape("<set name=\"varname\" value=\"\">") + "</code>"
},
{
"label": "Jet: &lt;if&gt;",
"html": "<code id=\"newitem\" contenteditable=\"true\" style=\"display:inline-block; background:#eef; border:1px dashed #99c; padding:4px; font-family:monospace; white-space:pre-wrap;\">" + jetEscape("<if value1=\"" + "$" + "[varname]\" value2=\"\" type=\"eq\">\ncontent\n</if>") + "</code>"
}
];
tbitems.forEach(doit);
function doit(currentValue, index) {
let index2 = index + 1;
let toolbar = document.getElementById('toolbar');
var itemHTML = '<button type="button" id="button' + index2;
itemHTML += '" style="width: 100%; height: 40px; border: 2px solid green;"';
itemHTML += '>' + currentValue.label + '</button>';
toolbar.insertAdjacentHTML("beforeend", itemHTML);
let id = 'button' + index2;
let button = document.getElementById(id);
button.html = currentValue.html;
button.addEventListener("mousedown",
(e) => {
e.preventDefault();
saveUndo();
draggingHTML = currentValue.html;
mainpage.insertAdjacentHTML('beforeend', draggingHTML);
let newEl = mainpage.lastElementChild;
newEl.style.position = 'absolute';
newEl.style.zIndex = '10000';
newEl.style.left = '0px';
newEl.style.top = '0px';
startNewItemDrag(newEl);
}
, false);
}
}
function startNewItemDrag(el) {
let rect = mainpage.getBoundingClientRect();
function ndMove(e) {
el.style.left = (e.clientX - rect.left - el.offsetWidth / 2) + 'px';
el.style.top = (e.clientY - rect.top - el.offsetHeight / 2) + 'px';
}
function ndUp(e) {
document.removeEventListener('mousemove', ndMove);
document.removeEventListener('mouseup', ndUp);
draggingHTML = null;
if((typeof selected !== 'undefined') && selected) {
selected.style.outline = '';
}
selected = el;
selected.style.outline = '2px solid #0088ff';
getLayerMap(selected);
displayParameters();
mode = "MOUSEUP";
}
document.addEventListener('mousemove', ndMove);
document.addEventListener('mouseup', ndUp);
}
function window_mousedown(win, e) {
mousedownx = e.offsetX;
mousedowny = e.offsetY;
if(e.offsetX >= (win.clientWidth - 3)) {
win.style.cursor = 'ew-resize';
window_dragging = true;
}
else {
win.style.cursor = 'default';
window_dragging = false;
}
}
function window_mousemove(win, e) {
}
function window_mouseup(win, e) {
}
function drawGrid() {
if(showGrid == true) {
context.clearRect(0,0,grid.width,grid.height);
context.lineWidth = 0.5;
context.strokeStyle = "#808080";
for(var ix = 0; ix < grid.width; ix += gridSize) {
context.beginPath();
context.moveTo(ix, 0);
context.lineTo(ix, grid.height);
context.stroke();
context.beginPath();
context.moveTo(0, ix);
context.lineTo(grid.width, ix);
context.stroke();
}
} else {
context.clearRect(0,0,grid.width,grid.height);
}
}
function resetDragHint(e) {
if(!selected) {
return;
}
if(selected.style.position == 'absolute') {
if((e.offsetX >= (parseFloat(selected.style.width) - 10)) && (e.offsetY >= (parseFloat(selected.style.height) - 10))) {
selected.style.cursor = "nwse-resize";
dragHint = "rightbottomresize";
} else if((e.offsetY < 10) && (e.offsetX >= (parseFloat(selected.style.width) - 10))) {
selected.style.cursor = "nesw-resize";
dragHint = "righttopresize";
} else if((e.offsetX <= 10) && (e.offsetY >= (parseFloat(selected.style.height) - 10))) {
selected.style.cursor = "nesw-resize";
dragHint = "leftbottomresize";
} else if((e.offsetX <= 10) && (e.offsetY <= 10)) {
selected.style.cursor = "nwse-resize";
dragHint = "lefttopresize";
} else if(e.offsetX >= (parseFloat(selected.style.width) - 10)) {
selected.style.cursor = "ew-resize";
dragHint = "rightresize";
} else if(e.offsetY >= (parseFloat(selected.style.height) - 10)) {
selected.style.cursor = "ns-resize";
dragHint = "bottomresize";
} else if(e.offsetX <= 10) {
selected.style.cursor = "ew-resize";
dragHint = "leftresize";
} else if(e.offsetY <= 10) {
selected.style.cursor = "ns-resize";
dragHint = "topresize";
} else {
selected.style.cursor = "move";
dragHint = "move";
}
} else {
if((e.offsetX >= (parseFloat(selected.style.width) - 10)) && (e.offsetY >= (parseFloat(selected.style.height) - 10))) {
selected.style.cursor = "nwse-resize";
dragHint = "rightbottomresize";
} else if(e.offsetX >= (parseFloat(selected.style.width) - 10)) {
selected.style.cursor = "ew-resize";
dragHint = "rightresize";
} else if(e.offsetY >= (parseFloat(selected.style.height) - 10)) {
selected.style.cursor = "ns-resize";
dragHint = "bottomresize";
} else {
selected.style.cursor = "move";
dragHint = "move";
}
}
}
function mousedown(e) {
let mouseabove = document.elementFromPoint(e.clientX, e.clientY);
if((typeof selected === 'undefined') || (selected != mouseabove)) {
mode = 'NEWSELECTION';
if((typeof selected !== 'undefined') && (selected.nodeName == "DIV")) {
document.designMode = 'off';
selected.contentEditable = false;
selected.blur();
selected.style.userSelect = 'none';
}
if((typeof selected !== 'undefined') && selected) {
selected.style.outline = '';
}
selected = mouseabove;
selected.style.outline = '2px solid #0088ff';
getLayerMap(selected);
} else {
if(mode != "TEXT") {
mode = "DOWNONSELECTED";
}
}
if(mode != 'TEXT') {
resetDragHint(e);
}
mousedownx = e.clientX;
mousedowny = e.clientY;
displayParameters();
return true;
}
function mouseup(e) {
if(mode == "TEXT") {
return true;
}
if(mode == "DOWNONSELECTED") {
if((selected.nodeName == "DIV") || (selected.nodeName == "CODE")) {
document.designMode = 'on';
selected.style.userSelect = 'auto';
selected.contentEditable = true;
selected.style.cursor = 'text';
selected.focus();
mode = "TEXT";
return true;
}
}
if(draggingHTML) {
draggingHTML = null;
}
if(selected && (mode == 'MOVED') && (selected.style.position == 'absolute')) {
selected.style.pointerEvents = 'none';
let dragover = document.elementFromPoint(e.clientX, e.clientY);
selected.style.pointerEvents = 'auto';
if(dragover.nodeName == 'DIV') {
if(dragover.id != selected.parentElement.id) {
dragover.appendChild(selected);
selected.style.left = ((selected.getBoundingClientRect().x + e.offsetX - mousedownx) - dragover.getBoundingClientRect().x) + 'px';
selected.style.top = ((selected.getBoundingClientRect().y + e.offsetY - mousedowny) - dragover.getBoundingClientRect().y) + 'px';
}
}
} else if(selected && (mode == 'MOVED') && pendingDragover) {
pendingDragover.prepend(selected);
selected.style.left = parseFloat(pendingDragover.style.left) + "px";
selected.style.top = parseFloat(pendingDragover.style.top) + "px";
}
pendingDragover = null;
hideDropIndicator();
mode = "MOUSEUP";
return true;
}
function mousemove(e) {
if(mode == "MOUSEUP") {
resetDragHint(e);
} else {
if(mode == "TEXT") {
return true;
}
if((selected.style.position == 'static') ||
(typeof selected.style.position === 'undefined') ||
(selected.style.position == '')) {
if(dragHint == "move") {
if(mode != "MOVED") {
mode = "MOVED";
}
selected.style.pointerEvents = 'none';
let dragover = document.elementFromPoint(e.clientX, e.clientY);
selected.style.pointerEvents = 'auto';
if(dragover.nodeName == 'DIV') {
pendingDragover = dragover;
showDropIndicator(dragover);
}
return true;
} else if(dragHint == "rightbottomresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.width = (parseFloat(selected.style.width) + e.movementX) + "px";
selected.style.height = (parseFloat(selected.style.height) + e.movementY) + "px";
} else if(dragHint == "rightresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.width = (parseFloat(selected.style.width) + e.movementX) + "px";
} else if(dragHint == "bottomresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.height = (parseFloat(selected.style.height) + e.movementY) + "px";
}
} else if(selected.style.position == 'absolute') {
if(dragHint == "move") {
if(mode != "MOVED") {
saveUndo();
mode = "MOVED";
}
selected.style.left = (parseFloat(selected.style.left) + e.movementX) + "px";
selected.style.top = (parseFloat(selected.style.top) + e.movementY) + "px";
} else if(dragHint == "rightbottomresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.width = (parseFloat(selected.style.width) + e.movementX) + "px";
selected.style.height = (parseFloat(selected.style.height) + e.movementY) + "px";
} else if(dragHint == "lefttopresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.left = (parseFloat(selected.style.left) + e.movementX) + "px";
selected.style.top = (parseFloat(selected.style.top) + e.movementY) + "px";
selected.style.width = (parseFloat(selected.style.width) - e.movementX) + "px";
selected.style.height = (parseFloat(selected.style.height) - e.movementY) + "px";
} else if(dragHint == "leftbottomresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.left = (parseFloat(selected.style.left) + e.movementX) + "px";
selected.style.width = (parseFloat(selected.style.width) - e.movementX) + "px";
selected.style.height = (parseFloat(selected.style.height) + e.movementY) + "px";
} else if(dragHint == "righttopresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.top = (parseFloat(selected.style.top) + e.movementY) + "px";
selected.style.height = (parseFloat(selected.style.height) - e.movementY) + "px";
selected.style.width = (parseFloat(selected.style.width) + e.movementX) + "px";
} else if(dragHint == "rightresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.width = (parseFloat(selected.style.width) + e.movementX) + "px";
} else if(dragHint == "bottomresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.height = (parseFloat(selected.style.height) + e.movementY) + "px";
} else if(dragHint == "leftresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.left = (parseFloat(selected.style.left) + e.movementX) + "px";
selected.style.width = (parseFloat(selected.style.width) - e.movementX) + "px";
} else if(dragHint == "topresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.top = (parseFloat(selected.style.top) + e.movementY) + "px";
selected.style.height = (parseFloat(selected.style.height) - e.movementY) + "px";
}
displayParameters();
}
return true;
}
}
function heading(name) {
let properties = document.getElementById('properties');
properties.insertAdjacentHTML('beforeend', "<tr><td colspan=\"2\" style=\"background-color: #abc; padding: 4px; font-weight: bold;\">" + name + ":</td></tr>");
}
function setColorProperty(event) {
selected.style.setProperty(event.target.id, document.getElementById(event.target.id).value);
}
function getColorProperty(cssprop) {
let value = selected.style.getPropertyValue(cssprop);
if(value.substring(0, 4) == "rgb(") {
document.getElementById(cssprop).value = value;
}
else {
selected.style.removeProperty(cssprop);
}
}
function setSizeProperty(event) {
var value = document.getElementById(event.target.id);
var units = document.getElementById(event.target.id + "Units");
if(value.value == "") {
units.value = "";
} else if(units && units.value == "") {
units.value = "px";
}
if(units) {
selected.style.setProperty(event.target.id, value.value + units.value);
}
}
function getSizeProperty(cssprop) {
let size = selected.style.getPropertyValue(cssprop);
if(size) {
document.getElementById(cssprop).value = parseInt(size);
document.getElementById(cssprop + "Units").value = size.replace(/[0-9]/g, '');
}
}
function property(name, payload, cssprop, initfun, changefun) {
let properties = document.getElementById('properties');
properties.insertAdjacentHTML('beforeend', "<tr><td style=\"background-color: #cdf; padding: 3px 4px; vertical-align: middle; white-space: nowrap;\">" + name + ":</td><td style=\"background-color: #cdf; padding: 3px 4px; vertical-align: middle;\">" + payload + "</td></tr>");
if(initfun) {
initfun(cssprop);
}
if(cssprop) {
document.getElementById(cssprop).addEventListener('input', changefun);
let prop = document.getElementById(cssprop + "Units");
if(prop) {
prop.addEventListener('change', changefun);
}
}
}
function propsize(title, cssprop) {
property(title,
"<div style=\"display: flex; align-items: center; gap: 4px;\">" +
"<input type=\"number\" id=\"" + cssprop + "\" style=\"flex: 1 1 auto; min-width: 0; box-sizing: border-box;\">" +
"<select id=\"" + cssprop + "Units\" style=\"flex: 0 0 auto;\">" +
" <option value=\"\"></option>" +
" <option value=\"px\">px</option>" +
" <option value=\"%\">%</option>" +
"</select></div>",
cssprop,
getSizeProperty,
setSizeProperty);
}
function propcolor(title, cssprop) {
property(title,
"<input type=\"color\" id=\"" + cssprop + "\" style=\"height: 18px; width: 50px; margin: 0px; padding: 0px; border: none; width:85%;\">",
cssprop,
getColorProperty,
setColorProperty);
}
function displayParameters() {
if(!selected) {
itemparameters.innerHTML = "";
return;
}
itemparameters.innerHTML = "<div style=\"width: 300px; max-width: 300px;\"><table id=\"properties\" style=\"width: 300px; table-layout: fixed;\"><colgroup><col style=\"width: 35%;\"><col style=\"width: 65%;\"></colgroup></table></div>";
property("Type", selected.nodeName);
property("Id", selected.id);
property("Display",
"<select id=\"display\" style=\"width: 100%; float: right;\">" +
" <option value=\"inline\">Inline</option>" +
" <option value=\"block\">Block</option>" +
" <option value=\"inline-block\">Inline Block</option>" +
" <option value=\"none\">None</option>" +
"</select>",
"display",
() => { document.getElementById('display').value = selected.style.display; },
() => { selected.style.display = document.getElementById('display').value; }
);
if(selected.id != "mainpage") {
heading("Geometry");
property("Position",
"<select id=\"position\" style=\"width: 100%; float: right;\">" +
" <option value=\"absolute\">Absolute</option>" +
" <option value=\"static\">Static</option>" +
" <option value=\"relative\">Relative</option>" +
"</select>",
"position",
() => { document.getElementById('position').value = selected.style.position; },
() => { saveUndo();
selected.style.position = document.getElementById('position').value;
if(document.getElementById('position').value == 'absolute') {
selected.style.left = selected.getBoundingClientRect().x + 'px';
selected.style.top = selected.getBoundingClientRect().y + 'px';
}
displayParameters();
}
);
if(selected.style.position == 'absolute') {
propsize("Left", "left");
propsize("Top", "top");
}
propsize("Width", "width");
propsize("Height", "height");
}
if(selected.nodeName != 'IMG') {
heading("Font");
propcolor("Color", "color");
}
heading("Margins");
propsize("Left", "margin-left");
propsize("Top", "margin-top");
propsize("Right", "margin-right");
propsize("Bottom", "margin-bottom");
if(selected.id != "mainpage") {
heading("Border");
propsize("Width", "border-width");
propcolor("Color", "border-color");
}
heading("Background");
propcolor("Color", "background-color");
heading("Padding");
propsize("Left", "padding-left");
propsize("Top", "padding-top");
propsize("Right", "padding-right");
propsize("Bottom", "padding-bottom");
}
function newImage(filename, e) {
saveUndo();
let img = '<img id="newitem" src="/assets/' + filename + '" style="width: 150px;">';
mainpage.insertAdjacentHTML('beforeend', img);
}
function handleFiles(files) {
}
function b64encode(str) {
return btoa(unescape(encodeURIComponent(str)));
}
function save(form) {
form.html.value = b64encode(document.getElementById('mainpage').innerHTML);
form.exit.value = '';
form.submit();
}
function savenexit(form) {
form.html.value = b64encode(document.getElementById('mainpage').innerHTML);
form.exit.value = 'yes';
form.submit();
}
function exitnosave(form) {
form.html.value = '';
form.exit.value = 'yes';
form.submit();
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.
+156 -1
View File
@@ -42,6 +42,31 @@
* { box-sizing: border-box; } * { box-sizing: border-box; }
* {
scrollbar-width: thin;
scrollbar-color: var(--bond-gold) var(--bond-bg);
}
::-webkit-scrollbar {
width: 11px;
height: 11px;
}
::-webkit-scrollbar-track {
background: var(--bond-bg);
}
::-webkit-scrollbar-thumb {
background-color: var(--bond-gold);
background-image: linear-gradient(180deg, var(--bond-gold-bright), var(--bond-gold));
background-clip: padding-box;
border: 2px solid var(--bond-bg);
border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover {
background-image: linear-gradient(180deg, var(--bond-gold-bright), var(--bond-gold-bright));
}
::-webkit-scrollbar-corner {
background: var(--bond-bg);
}
body { body {
background: radial-gradient(ellipse at top, #1a1a20 0%, #0a0a0d 60%, #060607 100%); background: radial-gradient(ellipse at top, #1a1a20 0%, #0a0a0d 60%, #060607 100%);
background-attachment: fixed; background-attachment: fixed;
@@ -99,6 +124,10 @@ li { font-family: 8pt exo2; color: var(--bond-text); }
padding: 0px; padding: 0px;
max-width: 1050px; max-width: 1050px;
min-width: 1050px; min-width: 1050px;
min-height: calc(100vh - 150px);
box-sizing: border-box;
display: flex;
flex-direction: column;
margin: auto; margin: auto;
font-family: exo2; font-family: exo2;
font-size: 10pt; font-size: 10pt;
@@ -106,6 +135,18 @@ li { font-family: 8pt exo2; color: var(--bond-text); }
overflow: hidden; overflow: hidden;
} }
/* The email client is the one page whose own content (a long message
list) can outgrow the viewport - everywhere else .pageclass is meant
to grow taller than the viewport for long content (normal document
scroll). Scoped to only this page via :has() so no other page's
layout changes: caps .pageclass at the same height it already uses
as a floor, so #email-list/#email-reading-pane's own overflow-y:auto
scrolls instead of the whole page growing past the viewport. */
.pageclass:has(#email-app) {
height: calc(100vh - 150px);
overflow: hidden;
}
input { input {
background: #101014; background: #101014;
color: var(--bond-text); color: var(--bond-text);
@@ -194,6 +235,19 @@ textarea.fieldinputerror {
background: var(--bond-line); background: var(--bond-line);
} }
.user-badge {
display: inline-block;
padding: 3px 12px;
margin-bottom: 4px;
border-radius: 6px;
border: 1px solid var(--bond-gold);
background: linear-gradient(180deg, rgba(197, 164, 90, 0.28), rgba(197, 164, 90, 0.12));
box-shadow: 0 0 0.5em rgba(197, 164, 90, 0.25);
color: var(--bond-gold-bright);
font-weight: 600;
letter-spacing: 0.03em;
}
.sectionframecontents { .sectionframecontents {
padding: 10px; padding: 10px;
text-align: justify; text-align: justify;
@@ -319,6 +373,28 @@ textarea.fieldinputerror {
border-radius: 3px; border-radius: 3px;
} }
#panelmenu-resizer {
flex-shrink: 0;
width: 9px;
cursor: col-resize;
position: relative;
background: var(--bond-line-soft);
border-radius: 3px;
}
#panelmenu-resizer:hover, #panelmenu-resizer.resizing {
background: var(--bond-gold);
}
#panelmenu-resizer svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12px;
height: 12px;
color: var(--bond-gold-bright);
pointer-events: none;
}
.panel table { .panel table {
font: 12px exo2; font: 12px exo2;
padding: 5px; padding: 5px;
@@ -398,11 +474,12 @@ textarea.fieldinputerror {
padding: 2px 10px 3px 10px; padding: 2px 10px 3px 10px;
margin: 0px; margin: 0px;
border-radius: 3px; border-radius: 3px;
box-shadow: 0 0 0.5em rgba(197, 164, 90, 0.3), 0 0 0.15em rgba(227, 196, 126, 0.4);
transition: box-shadow 0.2s ease, background 0.2s ease; transition: box-shadow 0.2s ease, background 0.2s ease;
} }
.button:hover { .button:hover {
box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.35); box-shadow: 0 0 0.9em rgba(197, 164, 90, 0.55), 0 0 0.3em rgba(227, 196, 126, 0.6);
background: linear-gradient(180deg, #24242e, #1a1a20); background: linear-gradient(180deg, #24242e, #1a1a20);
} }
@@ -688,3 +765,81 @@ textarea.fieldinputerror {
margin-top: 12px; margin-top: 12px;
min-height: 1.2em; min-height: 1.2em;
} }
/* Site-wide floating file transfer manager (see documents.js) - lives
outside #content in mainpagetemplate.jet so it survives SPA
navigation, letting an upload/download keep running while browsing
to a different page. */
#filemgr-panel {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9000;
font-family: exo2;
font-size: 9pt;
}
#filemgr-toggle {
width: 44px; height: 44px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
position: relative;
cursor: pointer;
border: 1px solid var(--bond-gold);
background: linear-gradient(180deg, #1c1c24, #131318);
color: var(--bond-gold-bright);
box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.35);
transition: box-shadow 0.2s ease, background 0.2s ease;
margin-left: auto;
}
#filemgr-toggle:hover {
box-shadow: 0 0 1em rgba(197, 164, 90, 0.6), 0 0 0.3em rgba(227, 196, 126, 0.6);
background: linear-gradient(180deg, #24242e, #1a1a20);
}
#filemgr-toggle svg { width: 20px; height: 20px; }
#filemgr-badge {
position: absolute; top: -4px; right: -4px;
min-width: 16px; height: 16px; padding: 0 3px;
border-radius: 8px;
background: var(--bond-gold-bright);
color: #131318;
font-size: 8pt; font-weight: bold;
display: flex; align-items: center; justify-content: center;
}
#filemgr-badge:empty { display: none; }
#filemgr-body {
display: none;
width: 320px;
max-height: 400px;
margin-bottom: 10px;
margin-left: auto;
border-radius: 8px;
border: 1px solid var(--bond-line);
background: var(--bond-bg-2);
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
overflow: hidden;
display: flex;
flex-direction: column;
}
#filemgr-panel.filemgr-expanded #filemgr-body { display: flex; }
#filemgr-header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 12px;
border-bottom: 1pt solid var(--bond-line-soft);
color: var(--bond-gold-bright);
font-weight: 600;
}
#filemgr-clear { font-weight: normal; font-size: 8pt; color: var(--bond-text-dim); cursor: pointer; }
#filemgr-clear:hover { color: var(--bond-text); }
#filemgr-list { overflow-y: auto; padding: 6px; }
#filemgr-empty { color: var(--bond-text-dim); padding: 10px; text-align: center; }
.filemgr-item { padding: 8px; border-radius: 6px; margin-bottom: 4px; background: rgba(255,255,255,0.03); }
.filemgr-item-row { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.filemgr-item-icon { flex-shrink: 0; width: 14px; height: 14px; color: var(--bond-gold-bright); }
.filemgr-item-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filemgr-item-status { flex-shrink: 0; font-size: 8pt; color: var(--bond-text-dim); }
.filemgr-item.filemgr-failed .filemgr-item-status { color: #c66; }
.filemgr-item.filemgr-done .filemgr-item-status { color: var(--bond-gold-bright); }
.filemgr-progress-track { height: 4px; border-radius: 2px; background: rgba(255,255,255,0.08); overflow: hidden; }
.filemgr-progress-fill { height: 100%; width: 0%; background: var(--bond-gold-bright); transition: width 0.15s ease; }
.filemgr-item.filemgr-done .filemgr-progress-fill { background: #6c6; }
.filemgr-item.filemgr-failed .filemgr-progress-fill { background: #c66; }
Binary file not shown.

After

Width:  |  Height:  |  Size: 815 KiB

@@ -0,0 +1,76 @@
(function() {
var form = document.getElementById('newemail-form');
if (!form)
return;
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 navigateTo(url) {
var a = document.createElement('a');
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
var body = document.getElementById('newemail-body');
var toolbar = document.getElementById('newemail-toolbar');
toolbar.addEventListener('click', function(e) {
var btn = e.target.closest('button');
if (!btn)
return;
body.focus();
var cmd = btn.getAttribute('data-cmd');
if (cmd === 'createLink') {
var url = window.prompt('Link URL:');
if (url)
document.execCommand('createLink', false, url);
} else {
document.execCommand(cmd, false, null);
}
});
var errorDiv = document.getElementById('newemail-error');
var sendBtn = document.getElementById('newemail-send-btn');
var cancelBtn = document.getElementById('newemail-cancel-btn');
cancelBtn.addEventListener('click', function() {
navigateTo('/admin/email');
});
sendBtn.addEventListener('click', function() {
var to = document.getElementById('newemail-to').value.trim();
var subject = document.getElementById('newemail-subject').value.trim();
var html = body.innerHTML.trim();
errorDiv.textContent = '';
if (!to) {
errorDiv.textContent = 'A recipient address is required.';
return;
}
if (!html) {
errorDiv.textContent = 'The message body is empty.';
return;
}
sendBtn.classList.add('newemail-sending');
post('/email/send', { to: to, subject: subject, body: html })
.then(function(res) {
sendBtn.classList.remove('newemail-sending');
if (!res.ok) {
errorDiv.textContent = res.error || 'Could not send the message.';
return;
}
navigateTo('/admin/email');
})
.catch(function() {
sendBtn.classList.remove('newemail-sending');
errorDiv.textContent = 'Could not send the message.';
});
});
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+322
View File
@@ -0,0 +1,322 @@
(function() {
var content = document.getElementById('content');
var form = document.getElementById('mainform');
// Lives outside #content (a sibling in the persistent shell, next to
// the menu bar/form) - a fragment swap never touches it on its own,
// so a per-page side menu would otherwise stay frozen on whichever
// page's menu happened to be there when the shell first loaded. See
// applyResponse()'s own handling of the #_spa_panelmenu marker below.
var panelMenu = document.getElementById('panelmenu');
// Same story as panelMenu just above, for the top-of-page tab bar and
// the upper-right "Welcome X / Logout" box - both live outside
// #content too, and both depend on server-side login state
// ($[_login_id], see menu.bar's own ACCOUNT/EMAIL/ADMIN tabs and
// login.menu's Welcome/Logout block) that only the server can know.
// Without this, logging in via a SPA form submit left the tab bar
// still showing LOGIN and the header box empty, even though the
// account page's own content came back correctly - only a full page
// reload picked up the new state, since only the non-fragment branch
// ever rendered these two includes at all.
var menuBar = document.getElementById('menubar');
var loginMenu = document.getElementById('loginmenu');
// The form's own initial action="<prefix>/<currentPage>" (server-
// rendered per-request - "/spa/$[PAGE]" during the 8081 test-path
// phase, "/$[PAGE]" once a port is cut over to the real path) tells
// us the route prefix this specific deployment is using, captured
// once here since the form itself lives outside #content and never
// gets re-rendered by a client-side navigation - only the initial
// page load sets it. Used to build the target for every subsequent
// submit (the PAGE hidden field's own value changes per navigation,
// this prefix does not) - keeps this one shared spa.js correct across
// every phase of the rollout without needing a matching edit each
// time a port's routing changes.
var formPrefix = '';
if (form) {
var initialAction = form.getAttribute('action') || '';
var initialPage = (form.elements['PAGE'] && form.elements['PAGE'].value) || '';
// action is "<prefix>/<page>" normally, or "<prefix>/<page>/<subpage>"
// when the page loaded directly on a URL that already has one (e.g.
// a bookmark to /opensource/barantmail) - suffix has to account for
// both or this initial load's own subpage would get mistaken for
// part of the prefix.
var initialSubpage = (form.elements['SUBPAGE'] && form.elements['SUBPAGE'].value) || '';
var initialSuffix = initialPage + (initialSubpage ? '/' + initialSubpage : '');
if (initialSuffix && initialAction.slice(-initialSuffix.length) === initialSuffix)
formPrefix = initialAction.slice(0, initialAction.length - initialSuffix.length - 1);
}
// The nav bar (menu.bar) lives in the persistent shell, outside
// #content - a fetch-and-swap navigation never re-renders it, so
// without this the "selected" tab highlight would just stay frozen
// on whichever page the shell happened to load with. Each tab's own
// <a href="/x"> (the real, un-prefixed target path - see menu.bar's
// own <tab page="x"><a href="/x">...</a></tab> shape) is what's used
// to identify it; toggling classes here keeps it in sync with every
// client-side navigation without needing the server to resend the
// whole menu bar on every fragment response.
function syncActiveTab(targetPath) {
if (formPrefix && targetPath.indexOf(formPrefix) === 0)
targetPath = targetPath.slice(formPrefix.length) || '/';
var tabs = document.querySelectorAll('.baritem, .baritemselected');
for (var i = 0; i < tabs.length; i++) {
// Each tab wraps TWO anchors: the outer one linkbutton generates
// (href="javascript:...", drives the actual click) and a real
// <a href="/x"> nested inside it as the visible link text/label
// (see menu.bar's own <tab page="x"><a href="/x">...</a></tab>
// shape). Both match a[href] - only the second, real-path one
// identifies the page, so the javascript: one has to be skipped
// explicitly rather than just taking the first match.
var links = tabs[i].querySelectorAll('a[href]');
var isActive = false;
for (var j = 0; j < links.length; j++) {
var href = links[j].getAttribute('href');
// Exact match covers a plain page; a subpage URL (e.g.
// /admin/users, /opensource/barantmail) never equals its own
// tab's bare /admin or /opensource href, so the tab lost its
// highlight the moment a subpage was selected. Matching on
// "targetPath starts with href, then either ends or continues
// with a /" keeps the tab highlighted for any of its own
// subpages without also matching an unrelated page whose name
// merely starts with the same characters (e.g. a hypothetical
// /administration would not match /admin).
if (href && href.indexOf('javascript:') !== 0 && href !== '/' &&
(href === targetPath ||
(targetPath.indexOf(href) === 0 &&
(targetPath.charAt(href.length) === '/' || targetPath.charAt(href.length) === '')))) {
isActive = true;
break;
}
}
tabs[i].classList.toggle('baritemselected', isActive);
tabs[i].classList.toggle('baritem', !isActive);
}
}
// Returns false if the response was a full-navigation redirect
// (already handled here, caller does nothing further); true if no
// data-spa-page marker was present at all (caller keeps using the
// URL it already requested); otherwise swaps in the content and
// returns {page, subpage} - the AUTHORITATIVE page/subpage the
// server actually rendered. mainpagetemplate.jet emits
// <div data-spa-page="$[PAGE]" data-spa-subpage="$[SUBPAGE]"> right
// before the fragment body, reflecting whatever a <process> may have
// changed PAGE to via <setpage> while handling the request (e.g. a
// successful login handing back the account page, not the login page
// the form was actually submitted to). Callers must use THIS value
// for history.pushState()/the active-tab highlight, not whatever
// page they originally requested - using the pre-request page here
// was exactly why a successful login left the address bar (and the
// tab highlight) stuck on /login even though the account page's
// content had correctly come back. subpage is the optional second
// URL segment (e.g. /opensource/barantmail) a page can use to pick
// what to render in its own second-tier content area.
function applyResponse(html) {
var redirect = /<div\s+data-spa-redirect="([^"]*)"/i.exec(html);
if (redirect) {
window.location.href = redirect[1];
return false;
}
content.innerHTML = html;
// <script> elements set via innerHTML are inert - the browser
// deliberately never executes them (both inline scripts and
// src="..." ones), only ones present when the HTML parser itself
// builds the DOM (a full page load). Pages whose own content
// depends on a <script> running - checkout's Square card-form
// wiring, email's UI logic - silently did nothing when reached via
// a client-side SPA swap (e.g. clicking "Check Out") instead of a
// full reload; only worked by accident on a fresh /checkout load.
// Re-creating each script element makes the browser actually run
// it. async=false on src="" ones preserves normal in-order
// execution (dynamically-inserted external scripts default to
// async otherwise), matching how a real page load would run them.
var scripts = content.querySelectorAll('script');
for (var i = 0; i < scripts.length; i++) {
var old = scripts[i];
var fresh = document.createElement('script');
for (var j = 0; j < old.attributes.length; j++)
fresh.setAttribute(old.attributes[j].name, old.attributes[j].value);
if (fresh.src) fresh.async = false;
fresh.textContent = old.textContent;
old.parentNode.replaceChild(fresh, old);
}
// #_spa_panelmenu carries the CURRENT page's own side-menu markup
// (mainpagetemplate.jet renders it into every fragment response
// specifically so this can pull it out) - extracted from the
// parsed DOM (not the raw html string) so nested tags inside a
// page's own .menu content can't break a naive string search.
// Removed from #content afterward since it only ever belonged in
// #panelmenu - it was just riding along in the fragment payload to
// get there.
var panelMenuMarker = content.querySelector('#_spa_panelmenu');
if (panelMenuMarker) {
if (panelMenu) panelMenu.innerHTML = panelMenuMarker.innerHTML;
panelMenuMarker.remove();
}
// #_spa_menubar/#_spa_loginmenu carry the CURRENT request's own
// login-state-dependent header markup, the same riding-along
// pattern as #_spa_panelmenu above - see the comment on
// menuBar/loginMenu's declaration for why this is needed at all.
var menuBarMarker = content.querySelector('#_spa_menubar');
if (menuBarMarker) {
if (menuBar) menuBar.innerHTML = menuBarMarker.innerHTML;
menuBarMarker.remove();
}
var loginMenuMarker = content.querySelector('#_spa_loginmenu');
if (loginMenuMarker) {
if (loginMenu) loginMenu.innerHTML = loginMenuMarker.innerHTML;
loginMenuMarker.remove();
}
var pageMarker = /data-spa-page="([^"]*)"/i.exec(html);
if (!pageMarker) return true;
var subpageMarker = /data-spa-subpage="([^"]*)"/i.exec(html);
return { page: pageMarker[1], subpage: subpageMarker ? subpageMarker[1] : '' };
}
// Shared by navigate() and the form submit handler: turns
// applyResponse()'s return value into the URL that should replace
// the address bar. `applied === true` (no marker in the response)
// keeps whatever URL was actually requested; an {page, subpage}
// object builds the authoritative one instead.
function resolvedUrl(applied, requestedUrl) {
if (applied === true) return requestedUrl;
return formPrefix + '/' + applied.page + (applied.subpage ? '/' + applied.subpage : '');
}
function delay(ms) {
return new Promise(function(resolve) { setTimeout(resolve, ms); });
}
// Fade-out/fade-in on every content swap (see the matching #content
// CSS transition/`.spa-fading` rule in main.css). The 180ms delay is
// paired with Promise.all so the fetch and the minimum fade-out time
// run concurrently, not back-to-back - a slow fetch just means the
// content stays faded a little longer, a fast one still shows the
// full fade rather than snapping instantly.
function navigate(url, push) {
var sep = url.indexOf('?') === -1 ? '?' : '&';
content.classList.add('spa-fading');
Promise.all([
fetch(url + sep + '_fragment=1', { credentials: 'same-origin' }).then(function(r) { return r.text(); }),
delay(180)
])
.then(function(results) {
var html = results[0];
var applied = applyResponse(html);
if (applied) {
var finalUrl = resolvedUrl(applied, url);
if (push) history.pushState({ spa: true }, '', finalUrl);
syncActiveTab(finalUrl.split('?')[0]);
}
content.classList.remove('spa-fading');
});
}
document.addEventListener('click', function(e) {
var a = e.target.closest('a');
if (!a) return;
var href = a.getAttribute('href');
if (!href || href.charAt(0) !== '/' || href.indexOf('javascript:') === 0) return;
if (a.target && a.target !== '' && a.target !== '_self') return;
e.preventDefault();
navigate(formPrefix + href, true);
});
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
var fd = new FormData(form);
var page = fd.get('PAGE') || '';
var subpage = fd.get('SUBPAGE') || '';
var url = formPrefix + '/' + page + (subpage ? '/' + subpage : '');
content.classList.add('spa-fading');
Promise.all([
fetch(url + '?_fragment=1', {
method: 'POST',
body: fd,
credentials: 'same-origin'
}).then(function(r) { return r.text(); }),
delay(180)
])
.then(function(results) {
var html = results[0];
var applied = applyResponse(html);
if (applied) {
var finalUrl = resolvedUrl(applied, url);
history.pushState({ spa: true }, '', finalUrl);
syncActiveTab(finalUrl.split('?')[0]);
}
content.classList.remove('spa-fading');
});
});
}
window.addEventListener('popstate', function() {
navigate(window.location.pathname + window.location.search, false);
});
// Drag-to-resize for the side menu (#panelmenu) against the main
// content column - the resizer bar (#panelmenu-resizer) lives in the
// persistent shell right alongside panelMenu itself (see its own
// declaration above), never touched by a fragment swap, so this only
// needs to be wired up once here, not re-attached per navigation.
// #content deliberately carries no fixed width of its own (see
// mainpagetemplate.jet) - it's a table-cell with nothing set, so it
// automatically takes up whatever's left in the row, meaning only
// panelMenu's own width ever needs to change by hand.
(function initPanelMenuResize() {
var resizer = document.getElementById('panelmenu-resizer');
if (!resizer || !panelMenu)
return;
var STORAGE_KEY = 'barant_panelmenu_width';
var MIN_WIDTH = 120;
var MAX_WIDTH = 400;
try {
var saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
var savedWidth = parseInt(saved, 10);
if (savedWidth >= MIN_WIDTH && savedWidth <= MAX_WIDTH)
panelMenu.style.width = savedWidth + 'px';
}
} catch (e) {
// Private-browsing/storage-disabled - just keep the default width.
}
var dragging = false;
var startX = 0;
var startWidth = 0;
resizer.addEventListener('mousedown', function(e) {
dragging = true;
startX = e.clientX;
startWidth = panelMenu.getBoundingClientRect().width;
resizer.classList.add('resizing');
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', function(e) {
if (!dragging)
return;
var newWidth = startWidth + (e.clientX - startX);
newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, newWidth));
panelMenu.style.width = newWidth + 'px';
});
document.addEventListener('mouseup', function() {
if (!dragging)
return;
dragging = false;
resizer.classList.remove('resizing');
document.body.style.userSelect = '';
try {
localStorage.setItem(STORAGE_KEY, Math.round(panelMenu.getBoundingClientRect().width).toString());
} catch (e) {
// Private-browsing/storage-disabled - the width still applies
// for the rest of this page view, just doesn't persist.
}
});
})();
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
for f in /home/barant/servercore-analysis/JetCore/jetc/barant-site/src/*.jet /home/barant/servercore-analysis/JetCore/jetc/barant-site/jet/*.jet; do
n=$(basename "$f")
case "$n" in
login_test*.jet|pagetest.jet) continue ;;
# These embed a full <!DOCTYPE html>...<body> document directly in
# their own raw source (mainpagetemplate.jet is the SPA shell;
# main.jet is a fully standalone page) - loading that raw source
# into htmleditor's contentEditable canvas via <read> means the
# browser's own HTML parser sees a real nested <html>/<body>,
# silently drops/merges those tags per normal parsing rules, and
# renders everything inside them as live chrome instead of showing
# it as editable source text. Every other .jet file here is a thin
# routing wrapper with no embedded document structure, so this
# only needs to exclude files that actually have one.
mainpagetemplate.jet|main.jet) continue ;;
esac
d=$(basename "$(dirname "$f")")
printf '<div style="padding:3px 0;"><a href="?dir=%s&amp;file=%s">%s/%s</a></div>\n' "$d" "$n" "$d" "$n"
done
+7 -1
View File
@@ -29,7 +29,7 @@
<tag name="linkbutton" keywords="pagedir page process name name2 value value2 target class multipart"> <tag name="linkbutton" keywords="pagedir page subpage process name name2 value value2 target class multipart">
<A HREF="javascript: <A HREF="javascript:
@@ -48,6 +48,12 @@
document.forms[0].PAGE.value='#[PAGE]'; document.forms[0].PAGE.value='#[PAGE]';
</IF> </IF>
<IF VALUE1="#[SUBPAGE]" VALUE2="" TYPE="EQ">
document.forms[0].SUBPAGE.value='';
<ELSE>
document.forms[0].SUBPAGE.value='#[SUBPAGE]';
</IF>
<IF VALUE1="#[NAME]" VALUE2="" TYPE="NE"> <IF VALUE1="#[NAME]" VALUE2="" TYPE="NE">
document.forms[0].#[NAME].value='#[VALUE]'; document.forms[0].#[NAME].value='#[VALUE]';
</IF> </IF>
+16
View File
@@ -0,0 +1,16 @@
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<sessionctl page="account">
<defaultnav>
<setpage name="account">
</defaultnav>
<link rel="stylesheet" href="/assets/main.css">
<link rel="stylesheet" href="/assets/control.css">
</sessionctl>
</database>
+138
View File
@@ -0,0 +1,138 @@
<style>
#email-connect-form { max-width: 360px; margin: 40px auto; }
#email-connect-form input { display: block; width: 100%; box-sizing: border-box; margin-bottom: 10px; padding: 6px; }
#email-connect-error { color: #a00; min-height: 1.2em; margin-bottom: 10px; }
#email-app { height: 100%; }
#email-client { display: flex; flex-direction: column; max-height: 100%; }
#email-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; border-bottom: 1pt solid var(--bond-line-soft); margin-bottom: 6px; }
#email-toolbar-actions { display: flex; align-items: center; gap: 6px; }
#email-search-box { display: flex; align-items: center; gap: 4px; margin-right: 6px; }
#email-search-input {
background: #101014; color: var(--bond-text);
border: 1px solid var(--bond-line-soft); border-radius: 4px;
padding: 4px 8px; font-size: 9pt; width: 150px;
}
#email-search-input:focus {
outline: none; border-color: var(--bond-gold-bright);
box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.5), 0 0 0.2em rgba(227, 196, 126, 0.55);
}
#email-list-count { color: var(--bond-text-dim); font-size: 9pt; margin-left: 6px; }
#email-layout { display: flex; flex: 1; min-height: 0; }
#email-list { width: 340px; flex-shrink: 0; overflow-y: auto; border-right: 1pt solid var(--bond-line-soft); transition: opacity 0.18s ease; }
#email-reading-pane { flex: 1; min-width: 0; overflow-y: auto; padding: 0 12px; transition: opacity 0.18s ease; }
.email-fading { opacity: 0; }
.email-row {
display: flex; align-items: center; gap: 6px; padding: 8px 10px 8px 13px;
border-bottom: 1pt solid var(--bond-line-soft); border-left: 3px solid transparent; cursor: pointer;
overflow: hidden;
transition: opacity 0.2s ease,
height 0.22s ease 0.2s, padding-top 0.22s ease 0.2s, padding-bottom 0.22s ease 0.2s,
margin-top 0.22s ease 0.2s, margin-bottom 0.22s ease 0.2s, border-width 0.22s ease 0.2s;
}
.email-row:hover { background: rgba(255, 255, 255, 0.05); }
.email-row.unread {
font-weight: bold;
background: rgba(197, 164, 90, 0.12);
}
.email-row.selected { background: rgba(197, 164, 90, 0.16); border-left-color: var(--bond-gold); }
.email-row.selected:hover { background: rgba(197, 164, 90, 0.22); }
/* Per-row removal animation for the trash button (see trashMessage()
in email.js) - dissolves in place first (the opacity transition,
no delay), then collapses its own box to nothing (the delayed
height/padding/margin/border transitions kick in once the fade is
mostly done) so every row below it slides up smoothly to close the
gap, purely as a side effect of ordinary document-flow reflow - no
other row is touched, faded, or re-rendered to make that happen. */
.email-row.email-row-removing {
opacity: 0;
height: 0 !important;
padding-top: 0; padding-bottom: 0;
margin-top: 0; margin-bottom: 0;
border-width: 0;
}
.email-row-info { flex: 1; min-width: 0; }
.email-row .email-from { font-size: 10pt; }
.email-row .email-subject { font-size: 9pt; color: var(--bond-text-dim); }
.email-row .email-date { font-size: 8pt; color: var(--bond-text-dim); }
.email-row-trash-btn { flex-shrink: 0; width: 24px; height: 24px; opacity: 0.55; }
.email-row-trash-btn svg { width: 13px; height: 13px; }
.email-row-trash-btn:hover { opacity: 1; }
#email-reading-pane .email-header { border-bottom: 1pt solid var(--bond-line-soft); margin-bottom: 10px; padding-bottom: 10px; }
#email-reading-pane .email-body { white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word; font-family: monospace; font-size: 9pt; }
#email-reading-pane .email-body-frame { width: 100%; height: 520px; border: none; border-radius: 4px; background: #fff; }
#email-reading-pane .email-actions { margin-top: 10px; }
#email-empty-state { color: var(--bond-text-dim); padding: 20px; }
#email-folder-list { display: flex; flex-direction: column; }
.email-folder-link { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px; margin-bottom: 2px; border-radius: 4px; text-decoration: none; color: inherit; }
.email-folder-link:hover { background: rgba(255,255,255,0.08); }
.email-folder-link.email-folder-selected { background: rgba(255,255,255,0.14); font-weight: bold; }
.email-folder-count { font-size: 8pt; padding: 1px 6px; border-radius: 8px; background: rgba(197, 164, 90, 0.25); color: var(--bond-gold-bright); }
.email-folder-count:empty { display: none; }
.email-account-link { display: block; padding: 6px 8px; margin-bottom: 2px; border-radius: 4px; text-decoration: none; color: inherit; font-size: 9pt; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.email-account-link:hover { background: rgba(255,255,255,0.08); }
.email-account-link.email-account-selected { background: rgba(197, 164, 90, 0.16); color: var(--bond-gold-bright); font-weight: bold; }
#email-add-account-link { display: block; padding: 6px 8px; font-size: 9pt; color: var(--bond-text-dim); text-decoration: none; cursor: pointer; }
#email-add-account-link:hover { color: var(--bond-text); }
#email-add-account-form { display: flex; flex-direction: column; gap: 6px; margin-bottom: 4px; }
#email-add-account-form input {
background: #101014; color: var(--bond-text);
border: 1px solid var(--bond-line-soft); border-radius: 4px;
padding: 5px 7px; font-size: 9pt; box-sizing: border-box; width: 100%;
}
#email-add-account-form input:focus {
outline: none; border-color: var(--bond-gold-bright);
box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.5), 0 0 0.2em rgba(227, 196, 126, 0.55);
}
#email-add-account-error { color: #a00; font-size: 8pt; min-height: 1.1em; }
.icon-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 30px; height: 30px; padding: 0; margin: 0; cursor: pointer;
border-radius: 6px; border: 1px solid var(--bond-gold);
background: linear-gradient(180deg, #1c1c24, #131318);
color: var(--bond-gold-bright);
box-shadow: 0 0 0.4em rgba(197, 164, 90, 0.25);
transition: box-shadow 0.2s ease, background 0.2s ease, transform 0.1s ease;
}
.icon-btn svg { width: 16px; height: 16px; display: block; }
.icon-btn:hover {
box-shadow: 0 0 0.8em rgba(197, 164, 90, 0.5), 0 0 0.25em rgba(227, 196, 126, 0.55);
background: linear-gradient(180deg, #24242e, #1a1a20);
}
.icon-btn:active { transform: scale(0.94); }
.icon-btn.icon-btn-active {
color: #131318;
background: linear-gradient(180deg, var(--bond-gold-bright), var(--bond-gold));
box-shadow: 0 0 0.7em rgba(197, 164, 90, 0.6);
}
</style>
<div id="email-app">
<if value1="$[@_imap_user]" value2="" type="eq">
<div id="email-connect-form">
<h3>Connect to Mailbox</h3>
<input type="text" id="email-user-input" placeholder="you@barant.com" autocomplete="username">
<input type="password" id="email-password-input" placeholder="Password" autocomplete="current-password">
<button type="button" id="email-connect-btn">Connect</button>
<div id="email-connect-error"></div>
</div>
<else>
<div id="email-client">
<div id="email-toolbar">
<span id="email-connected-as">$[@_imap_user]</span><span id="email-list-count"></span>
<span id="email-toolbar-actions">
<span id="email-search-box">
<input type="text" id="email-search-input" placeholder="Search mail&hellip;">
<button type="button" id="email-search-btn" class="icon-btn" title="Search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></svg></button>
</span>
<button type="button" id="email-compose-btn" class="icon-btn" title="New Mail"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h13a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/><path d="M2.5 7.5 12 14l9.5-6.5"/><path d="M18.5 3.5v5M16 6h5"/></svg></button>
<button type="button" id="email-refresh-btn" class="icon-btn" title="Refresh"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 0 0-14.6-4.4M4 4v5h5"/><path d="M4 13a8 8 0 0 0 14.6 4.4M20 20v-5h-5"/></svg></button>
<button type="button" id="email-disconnect-btn" class="icon-btn" title="Disconnect"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/></svg></button>
</span>
</div>
<div id="email-layout">
<div id="email-list"></div>
<div id="email-reading-pane"><div id="email-empty-state">Select a message to read it.</div></div>
</div>
</div>
</if>
</div>
<script src="/assets/email.js?v=20260905o"></script>
@@ -1,14 +1,5 @@
<page secure="yes" loggedin="yes" admin="true">
<title>ADMIN INVOICES</title>
<contents>
<eval name="_menu">
<include file="adminmenu.section">
</eval>
<retain name="invoice_id"> <retain name="invoice_id">
<h3>Invoices</h3> <h3>Invoices</h3>
<showtable page="admin_invoice" process="selectinvoice" name="invoice_id" value="X"> <showtable page="admin_invoice" process="selectinvoice" name="invoice_id" value="X">
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 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
</showtable> </showtable>
<br>
</contents>
</page>
@@ -0,0 +1,81 @@
<style>
#newemail-app { max-width: 720px; margin: 0 auto; }
#newemail-notconnected { color: var(--bond-text-dim); padding: 20px; }
#newemail-form { display: flex; flex-direction: column; gap: 10px; }
.newemail-field { display: flex; align-items: center; gap: 8px; }
.newemail-field label { width: 70px; flex-shrink: 0; color: var(--bond-text-dim); font-size: 9pt; }
.newemail-field input {
flex: 1; background: #101014; color: var(--bond-text);
border: 1px solid var(--bond-line-soft); border-radius: 4px;
padding: 6px 8px; font-size: 10pt; box-sizing: border-box;
}
.newemail-field input:focus {
outline: none; border-color: var(--bond-gold-bright);
box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.5), 0 0 0.2em rgba(227, 196, 126, 0.55);
}
#newemail-from { color: var(--bond-text-dim); font-size: 9pt; }
#newemail-toolbar { display: flex; gap: 4px; margin-top: 4px; }
#newemail-toolbar button {
width: 30px; height: 30px; padding: 0; cursor: pointer;
border-radius: 6px; border: 1px solid var(--bond-gold);
background: linear-gradient(180deg, #1c1c24, #131318);
color: var(--bond-gold-bright);
font-weight: bold; font-size: 11pt;
box-shadow: 0 0 0.4em rgba(197, 164, 90, 0.25);
transition: box-shadow 0.2s ease, background 0.2s ease, transform 0.1s ease;
}
#newemail-toolbar button:hover {
box-shadow: 0 0 0.8em rgba(197, 164, 90, 0.5), 0 0 0.25em rgba(227, 196, 126, 0.55);
background: linear-gradient(180deg, #24242e, #1a1a20);
}
#newemail-toolbar button:active { transform: scale(0.94); }
#newemail-body {
min-height: 260px; background: #fff; color: #000;
border: 1px solid var(--bond-line-soft); border-radius: 4px;
padding: 10px; font-size: 10pt; box-sizing: border-box;
overflow-y: auto;
}
#newemail-body:focus { outline: none; border-color: var(--bond-gold-bright); }
#newemail-error { color: #a00; min-height: 1.2em; }
#newemail-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 6px; }
#newemail-actions button {
padding: 8px 16px; cursor: pointer; border-radius: 6px;
border: 1px solid var(--bond-gold);
background: linear-gradient(180deg, #1c1c24, #131318);
color: var(--bond-gold-bright);
box-shadow: 0 0 0.4em rgba(197, 164, 90, 0.25);
transition: box-shadow 0.2s ease, background 0.2s ease, transform 0.1s ease;
}
#newemail-actions button:hover {
box-shadow: 0 0 0.8em rgba(197, 164, 90, 0.5), 0 0 0.25em rgba(227, 196, 126, 0.55);
background: linear-gradient(180deg, #24242e, #1a1a20);
}
#newemail-actions button:active { transform: scale(0.97); }
#newemail-send-btn.newemail-sending { opacity: 0.6; pointer-events: none; }
</style>
<div id="newemail-app">
<if value1="$[@_imap_user]" value2="" type="eq">
<div id="newemail-notconnected">Connect to your mailbox first (see the Email section) before composing a message.</div>
<else>
<h3>New Message</h3>
<div id="newemail-form">
<div class="newemail-field"><label>From</label><span id="newemail-from">$[@_imap_user]</span></div>
<div class="newemail-field"><label>To</label><input type="text" id="newemail-to" placeholder="recipient@example.com"></div>
<div class="newemail-field"><label>Subject</label><input type="text" id="newemail-subject" placeholder="Subject"></div>
<div id="newemail-toolbar">
<button type="button" data-cmd="bold" title="Bold"><b>B</b></button>
<button type="button" data-cmd="italic" title="Italic"><i>I</i></button>
<button type="button" data-cmd="underline" title="Underline"><u>U</u></button>
<button type="button" data-cmd="insertUnorderedList" title="Bulleted list">&#8226;</button>
<button type="button" data-cmd="createLink" title="Link">&#128279;</button>
</div>
<div id="newemail-body" contenteditable="true"></div>
<div id="newemail-error"></div>
<div id="newemail-actions">
<button type="button" id="newemail-cancel-btn">Cancel</button>
<button type="button" id="newemail-send-btn">Send</button>
</div>
</div>
</if>
</div>
<script src="/assets/newemail.js?v=20260905a"></script>
@@ -0,0 +1,4 @@
<h3>Products</h3>
<showtable page="editproduct" process="selectproduct" name="sku" value="X">
select sku as X,sku,type,description from products
</showtable>
+4
View File
@@ -0,0 +1,4 @@
<h3>Users</h3>
<showtable page="edituser" process="selectuser" name="selected_user_id" value="X">
select id as X,email,name,type,created,status from users
</showtable>
+16
View File
@@ -0,0 +1,16 @@
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<sessionctl page="admin">
<defaultnav>
<setpage name="admin">
</defaultnav>
<link rel="stylesheet" href="/assets/main.css">
<link rel="stylesheet" href="/assets/control.css">
</sessionctl>
</database>
+9
View File
@@ -0,0 +1,9 @@
<sectionframe title="ADMINISTRATIVE SUPPORT">
<linkbutton page="admin" subpage="email">EMAIL</linkbutton><br>
<linkbutton page="admin" subpage="invoices">INVOICES</linkbutton><br>
<linkbutton page="admin" subpage="products">PRODUCTS</linkbutton><br>
<linkbutton page="admin" subpage="users">USERS</linkbutton><br>
</sectionframe>
<if value1="$[SUBPAGE]" value2="email" type="eq">
<include file="email.menu">
</if>
+38 -10
View File
@@ -1,17 +1,45 @@
<page secure="yes" loggedin="yes" admin="true"> <page secure="yes" loggedin="yes">
<title>ADMINISTRATION</title> <title>ADMINISTRATION</title>
<contents> <contents>
<if value1="$[SUBPAGE]" value2="email" type="eq">
<include file="admin-email.subpage">
<else>
<if value1="$[SUBPAGE]" value2="newemail" type="eq">
<include file="admin-newemail.subpage">
<else>
<if value1="$[SUBPAGE]" value2="invoices" type="eq">
<if expr="('$[_login_type]'='user')">
<p>Requested page is not found.</p>
<else>
<include file="admin-invoices.subpage">
</if>
<else>
<if value1="$[SUBPAGE]" value2="users" type="eq">
<if expr="('$[_login_type]'='user')">
<p>Requested page is not found.</p>
<else>
<include file="admin-users.subpage">
</if>
<else>
<if value1="$[SUBPAGE]" value2="products" type="eq">
<if expr="('$[_login_type]'='user')">
<p>Requested page is not found.</p>
<else>
<include file="admin-products.subpage">
</if>
<else>
<if expr="('$[_login_type]'='user')">
<p>Requested page is not found.</p>
<else>
<eval name="_menu"> <eval name="_menu">
<include file="adminmenu.section"> <include file="adminmenu.section">
</eval> </eval>
<h3>Users</h3> <p>Select an item from the menu.</p>
<showtable page="edituser" process="selectuser" name="selected_user_id" value="X"> </if>
select id as X,email,name,type,created,status from users </if>
</showtable> </if>
<h3>Products</h3> </if>
<showtable page="editproduct" process="selectproduct" name="sku" value="X"> </if>
select sku as X,sku,type,description from products </if>
</showtable>
<br>
</contents> </contents>
</page> </page>
-23
View File
@@ -1,23 +0,0 @@
<page>
<title>BARANTMAIL OPEN SOURCE PROJECT</title>
<contents>
<sectionframe title="BARANTMAIL OPEN SOURCE PROJECT">
<p>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.</p>
<p>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.</p>
<p>The result is BARANTMAIL.</p>
<h2>Features</h2>
<p>Features of BARANTMAIL are:</p>
<ul>
<li>Fully compliant e-mail system.</li>
<li>Support for multiple domains.</li>
</ul>
<p></p>
</sectionframe>
</contents>
</page>
+42
View File
@@ -0,0 +1,42 @@
<page>
<title>BARANTSTORE</title>
<style>
#storefront-grid { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 14px; }
.storefront-item { width: 220px; border: 1px solid var(--bond-line); border-radius: 8px; padding: 12px; background: var(--bond-bg-2); }
.storefront-item img { width: 100%; height: 140px; object-fit: contain; background: #fff; border-radius: 4px; }
.storefront-item h4 { margin: 8px 0 4px 0; font-size: 10pt; min-height: 26px; }
.storefront-price { color: var(--bond-gold-bright); font-weight: bold; margin-bottom: 8px; }
</style>
<contents>
<sectionframe title="BarantStore">
<h1>BarantStore</h1>
<p>A demonstration storefront built on BARANT's own e-commerce
platform - browse and purchase real products below.</p>
<sql sessionid="">
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
</sql>$[sqlerror]
<ifrow sessionid="">
<div id="storefront-grid">
<whilerow sessionid="">
<div class="storefront-item">
<if value1="$[.image]" value2="" type="ne">
<img src="/assets/$[.image]">
</if>
<h4>$[.description]</h4>
<div class="storefront-price">$$[.price]</div>
<linkbutton process="addtocart" name="sku" value="$[.sku_prefix]"><div class="button">Add to Cart</div></linkbutton>
</div>
</whilerow>
</div>
<else>
<p>No products available right now.</p>
</ifrow>
</sectionframe>
</contents>
</page>
+16
View File
@@ -0,0 +1,16 @@
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<sessionctl page="cart">
<defaultnav>
<setpage name="cart">
</defaultnav>
<link rel="stylesheet" href="/assets/main.css">
<link rel="stylesheet" href="/assets/control.css">
</sessionctl>
</database>
@@ -0,0 +1,8 @@
<set name="lettersdir" value="/home/barant/servercore-analysis/JetCore/jetc/barant-site/letters">
<if value1="$[filename]" value2="" type="ne">
<if expr="(locate('/','$[filename]') = 0) and (locate('..','$[filename]') = 0)">
<write file="$[lettersdir]/$[filename].jet" mode="overwrite" value="">
<set name="id" value="$[filename]">
</if>
</if>
<setpage name="htmleditor">
+2
View File
@@ -0,0 +1,2 @@
#api GET /documents/download
<if expr="locate('/documents/download','$[REQUEST_URI]') = 1"><if value1="$[@_login_id]" value2="" type="eq">Not authorized.<else><set name="host" value="localhost"><set name="database" value="barant"><set name="user" value="barant"><set name="password" value="uversa1960"><database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid=""><sql sessionid="">select id,name from documents where id=$[id] and user_id=$[@_login_id]</sql><ifrow sessionid=""><header name="Content-Type" value="application/octet-stream"><header name="Content-Disposition" value="attachment; filename=$[.name]"><read file="./documents-barant-site/$[@_login_id]_$[id]" name="_filedata">$[_filedata]<else>Document not found.</ifrow></database></if><else>Unknown route.</if>
+29
View File
@@ -11,6 +11,26 @@
border-radius: 20px; border-radius: 20px;
} }
.number {font-size: 30px;} .number {font-size: 30px;}
#documents-dropzone {
border: 2px dashed var(--bond-line);
border-radius: 8px;
padding: 24px;
text-align: center;
color: var(--bond-text-dim);
margin-top: 10px;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
}
#documents-dropzone:hover, #documents-dropzone.documents-dropzone-active {
border-color: var(--bond-gold);
background: rgba(197, 164, 90, 0.06);
}
#documents-list { width: 100%; border-collapse: collapse; margin-top: 14px; }
#documents-list th, #documents-list td { padding: 6px 8px; border-bottom: 1pt solid var(--bond-line-soft); text-align: left; }
#documents-list th { color: var(--bond-text-dim); font-weight: normal; font-size: 9pt; }
#documents-list td a, #documents-list td .documents-action { color: var(--bond-gold-bright); cursor: pointer; text-decoration: none; }
#documents-list td a:hover, #documents-list td .documents-action:hover { text-decoration: underline; }
#documents-empty { color: var(--bond-text-dim); padding: 10px 0; }
</style> </style>
<contents> <contents>
<sectionframe title="Document Management"> <sectionframe title="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 group you can establish accounts and store correspondence and
documents for any activity with that account.</p> documents for any activity with that account.</p>
</sectionframe> </sectionframe>
<if value1="$[@_login_id]" value2="" type="ne">
<sectionframe title="Your Documents">
<input type="file" id="documents-file-input" multiple style="display:none;">
<div id="documents-dropzone">Drop files here, or click to choose files to upload</div>
<div id="documents-list-body"><div id="documents-empty">Loading&hellip;</div></div>
</sectionframe>
<script src="/assets/documents-page.js?v=20260906a"></script>
</if>
</contents> </contents>
</page> </page>
+52
View File
@@ -0,0 +1,52 @@
#api POST /documents/api/upload
#api POST /documents/api/delete
#api POST /documents/api/list
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<if value1="$[REQUEST_URI]" value2="/documents/api/upload" type="eq">
<header name="Content-Type" value="application/json">
<if value1="$[@_login_id]" value2="" type="eq">
{"ok":false,"error":"Not authorized."}
<else>
<if value1="$[name]" value2="" type="eq">
{"ok":false,"error":"Name is required."}
<else>
<sql sessionid="">insert into documents set user_id=$[@_login_id], name="$[name]", created=now()</sql>
<sql sessionid="">select last_insert_id() as document_id</sql>
<write file="./documents-barant-site/$[@_login_id]_$[document_id]" mode="overwrite" value="$[filedata]">
{"ok":true,"id":$[document_id],"name":"$[name]"}
</if>
</if>
<else>
<if value1="$[REQUEST_URI]" value2="/documents/api/delete" type="eq">
<header name="Content-Type" value="application/json">
<if value1="$[@_login_id]" value2="" type="eq">
{"ok":false,"error":"Not authorized."}
<else>
<sql sessionid="">select id from documents where id=$[id] and user_id=$[@_login_id]</sql>
<ifrow sessionid="">
<sql sessionid="">delete from documents where id=$[id] and user_id=$[@_login_id]</sql>
{"ok":true}
<else>
{"ok":false,"error":"Document not found."}
</ifrow>
</if>
<else>
<if value1="$[REQUEST_URI]" value2="/documents/api/list" type="eq">
<header name="Content-Type" value="application/json">
<if value1="$[@_login_id]" value2="" type="eq">
{"ok":false,"error":"Not authorized."}
<else>
<sql sessionid="">select id,name,created from documents where user_id=$[@_login_id] order by created desc</sql>
<set name="_doc_count" value="0">
{"ok":true,"documents":[<whilerow sessionid=""><if value1="$[_doc_count]" value2="0" type="ne">,</if>{"id":$[.id],"name":"$[.name]","created":"$[.created]"}<set name="_doc_count" value="1"></whilerow>]}
</if>
<else>
{"ok":false,"error":"Unknown route."}
</if>
</if>
</if>
</database>
@@ -0,0 +1,11 @@
<process>
<if value1="$[@_login_id]" value2="" type="ne">
<if value1="$[delete_id]" value2="" type="ne">
<sql sessionid="">select id from documents where id=$[delete_id] and user_id=$[@_login_id]</sql>$[sqlerror]
<ifrow sessionid="">
<sql sessionid="">delete from documents where id=$[delete_id] and user_id=$[@_login_id]</sql>$[sqlerror]
</ifrow>
</if>
</if>
<setpage name="documents">
</process>
@@ -0,0 +1,10 @@
<process>
<if value1="$[@_login_id]" value2="" type="ne">
<if value1="$[name]" value2="" type="ne">
<sql sessionid="">insert into documents set user_id=$[@_login_id], name="$[name]", created=now()</sql>$[sqlerror]
<sql sessionid="">select last_insert_id() as document_id</sql>
<write file="./documents-barant-site/$[@_login_id]_$[document_id]" mode="overwrite" value="$[filedata]">
</if>
</if>
<setpage name="documents">
</process>
+31
View File
@@ -0,0 +1,31 @@
<page secure="yes" loggedin="yes" admin="true">
<title>EDIT PRODUCT</title>
<contents>
<database host="$[host]" name="$[database]" user="$[user]" password="$[password]" sessionid="1">
<eval name="_menu">
<include file="adminmenu.section">
</eval>
<sql sessionid="1">select * from products where sku='$[_product_sku]' and sku_suffix=''</sql>$[sqlerror]
<ifrow sessionid="1">
<h3>Product $[1.sku]</h3>
<entryform>
<tr><td class="fieldprompt">Type:</td><td>
<select name="type">
<option value="PHYSICAL" <if value1="$[1.type]" value2="PHYSICAL" type="eq">selected</if>>PHYSICAL</option>
<option value="STREAMING" <if value1="$[1.type]" value2="STREAMING" type="eq">selected</if>>STREAMING</option>
<option value="SUBSCRIPTION" <if value1="$[1.type]" value2="SUBSCRIPTION" type="eq">selected</if>>SUBSCRIPTION</option>
<option value="FILE" <if value1="$[1.type]" value2="FILE" type="eq">selected</if>>FILE</option>
</select>
</td></tr>
<field prompt="Description" type="text" name="description" size="50" maxlength="50" value="$[1.description]">
<field prompt="Details" type="multiline" name="text" value="$[1.text]">
<field prompt="Price" type="text" name="price" size="15" maxlength="15" value="$[1.price]">
<field prompt="Image" type="text" name="image" size="50" maxlength="255" value="$[1.image]">
<buttonbay>
<linkbutton process="update_product"><div class="button">Update Product</div></linkbutton>
</buttonbay>
</entryform>
</ifrow>
</database>
</contents>
</page>
+178
View File
@@ -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
<header name="Content-Type" value="application/json">
<if value1="$[REQUEST_URI]" value2="/email/connect" type="eq">
<try>
<imap config="./email.cfg" user="$[user]" password="$[password]" sessionid="">
<eval name="_imap_accounts"><imapaccounts action="add" list="$[@_imap_accounts]" user="$[user]" password="$[password]"></imapaccounts></eval>
<set name="@_imap_accounts" value="$[_imap_accounts]">
<set name="_imap_user" value="$[user]">
<set name="@_imap_user" value="$[user]">
<set name="_imap_password" value="$[password]">
<set name="@_imap_password" value="$[password]">
{"ok":true}
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/disconnect" type="eq">
<if value1="$[@_imap_user]" value2="" type="ne">
<imap config="./email.cfg" user="$[@_imap_user]" disconnect="yes" sessionid="">
</imap>
<eval name="_imap_accounts"><imapaccounts action="remove" list="$[@_imap_accounts]" user="$[@_imap_user]"></imapaccounts></eval>
<set name="@_imap_accounts" value="$[_imap_accounts]">
</if>
<eval name="_next_user"><imapaccounts action="firstuser" list="$[@_imap_accounts]"></imapaccounts></eval>
<if value1="$[_next_user]" value2="" type="eq">
<set name="@_imap_user" value="">
<set name="@_imap_password" value="">
{"ok":true,"active":""}
<else>
<try>
<eval name="_next_password"><imapaccounts action="getpassword" list="$[@_imap_accounts]" user="$[_next_user]"></imapaccounts></eval>
<imap config="./email.cfg" user="$[_next_user]" password="$[_next_password]" sessionid="">
<set name="@_imap_user" value="$[_next_user]">
<set name="@_imap_password" value="$[_next_password]">
{"ok":true,"active":"$[_next_user]"}
</imap>
<catch name="_disconnecterr">
<set name="@_imap_user" value="">
<set name="@_imap_password" value="">
{"ok":true,"active":""}
</catch>
</try>
</if>
<else>
<if value1="$[@_imap_user]" value2="" type="eq">
{"ok":false,"error":"Not connected to a mailbox."}
<else>
<if value1="$[folder]" value2="" type="eq">
<set name="folder" value="INBOX">
</if>
<if value1="$[REQUEST_URI]" value2="/email/list" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<if value1="$[limit]" value2="" type="eq">
<set name="limit" value="50">
</if>
<imaplist sessionid="" folder="$[folder]" limit="$[limit]">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/message" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapmessage sessionid="" folder="$[folder]" uid="$[uid]">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/flag" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapflag sessionid="" folder="$[folder]" uid="$[uid]" flag="$[flag]" set="$[set]">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/delete" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapdelete sessionid="" folder="$[folder]" uid="$[uid]">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/trash" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapmove sessionid="" folder="$[folder]" uid="$[uid]" destfolder="Trash">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/folders" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapfolders sessionid="">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/search" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapsearch sessionid="" folder="$[folder]" query="$[query]">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/send" type="eq">
<try>
<smtpsend config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" to="$[to]" subject="$[subject]" html="$[body]">
<catch name="_smtperr">
{"ok":false,"error":"$[_smtperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/accounts" type="eq">
<imapaccounts action="list" list="$[@_imap_accounts]" active="$[@_imap_user]"></imapaccounts>
<else>
<if value1="$[REQUEST_URI]" value2="/email/switchaccount" type="eq">
<try>
<eval name="_switch_password"><imapaccounts action="getpassword" list="$[@_imap_accounts]" user="$[user]"></imapaccounts></eval>
<if value1="$[_switch_password]" value2="" type="eq">
{"ok":false,"error":"That account is not connected."}
<else>
<imap config="./email.cfg" user="$[user]" password="$[_switch_password]" sessionid="">
<set name="@_imap_user" value="$[user]">
<set name="@_imap_password" value="$[_switch_password]">
{"ok":true}
</imap>
</if>
<catch name="_switcherr">
{"ok":false,"error":"$[_switcherr]"}
</catch>
</try>
<else>
{"ok":false,"error":"Unknown route."}
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
+17
View File
@@ -0,0 +1,17 @@
<sectionframe title="MAILBOX">
<div id="email-folder-list">
<a href="javascript:void(0);" class="email-folder-link" data-folder="Inbox" onclick="window.emailSelectFolder && window.emailSelectFolder('Inbox'); return false;"><span>Inbox</span><span class="email-folder-count" data-folder-count="Inbox"></span></a>
<a href="javascript:void(0);" class="email-folder-link" data-folder="Spam" onclick="window.emailSelectFolder && window.emailSelectFolder('Spam'); return false;"><span>Spam</span><span class="email-folder-count" data-folder-count="Spam"></span></a>
<a href="javascript:void(0);" class="email-folder-link" data-folder="Trash" onclick="window.emailSelectFolder && window.emailSelectFolder('Trash'); return false;"><span>Trash</span><span class="email-folder-count" data-folder-count="Trash"></span></a>
</div>
</sectionframe>
<sectionframe title="ACCOUNTS">
<div id="email-account-list"></div>
<div id="email-add-account-form" style="display:none;">
<input type="text" id="email-add-account-user" placeholder="you@barant.com" autocomplete="username">
<input type="password" id="email-add-account-password" placeholder="Password" autocomplete="current-password">
<button type="button" id="email-add-account-btn">Connect</button>
<div id="email-add-account-error"></div>
</div>
<a href="javascript:void(0);" id="email-add-account-link" onclick="window.emailShowAddAccount && window.emailShowAddAccount(); return false;">+ Add Account</a>
</sectionframe>
+16
View File
@@ -0,0 +1,16 @@
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<sessionctl page="htmleditor">
<defaultnav>
<setpage name="htmleditor">
</defaultnav>
<link rel="stylesheet" href="/assets/main.css">
<link rel="stylesheet" href="/assets/control.css">
</sessionctl>
</database>
+154
View File
@@ -0,0 +1,154 @@
<page secure="yes" loggedin="yes">
<title>HTML EDITOR</title>
<contents>
<script src="/assets/htmleditor.js"></script>
<set name="sitedir" value="/home/barant/servercore-analysis/JetCore/jetc/barant-site">
<if value1="$[dir]" value2="" type="ne">
<if value1="$[file]" value2="" type="ne">
<if expr="(('$[dir]'='src') or ('$[dir]'='jet')) and (locate('/','$[file]') = 0) and (locate('..','$[file]') = 0)">
<try>
<read file="$[sitedir]/$[dir]/$[file]" name="mhtml">
<set name="letter_dir" value="$[dir]">
<set name="letter_file" value="$[file]">
<catch>
<set name="letter_file" value="">
</try>
<else>
<set name="letter_file" value="">
</if>
<else>
<set name="letter_file" value="">
</if>
<else>
<set name="letter_file" value="">
</if>
<if value1="$[letter_file]" value2="" type="eq">
<div style="max-width: 700px; margin: 40px auto; font-family: Helvetica;">
<h1>SITE .JET FILES</h1>
<p>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.</p>
<call pgm="$[sitedir]/list_jet_files.sh" name="filelist">
$[filelist]
</div>
<else>
<body ondragstart="return false;" ondrop="return false;" onload="init(); return true;">
<form method="post" action="$[REQUEST_URI]">
<input type="hidden" name="PROCESS" value="savehtmleditor">
<input type="hidden" name="html" value="">
<input type="hidden" name="exit" value="">
<input type="hidden" name="letter_dir" value="$[letter_dir]">
<input type="hidden" name="letter_file" value="$[letter_file]">
<div style="position: relative;">
<div id="window1" style="position: absolute; left: 0px; top: 0px; border: 1pt solid black; display: inline-block; padding: 2px; background: #808080; color: #ffffff;"
onmousemove="window_mousemove(this, event); return true;"
onmousedown="window_mousedown(this, event); return true;"
onmouseup="window_mouseup(this, event); return true;">
<div style="background-color: #678; height: 21px; padding: 2px 2px 2px 2px;">
<div style="float: left;"><b style="padding: 2px 8px;">$[letter_dir]/$[letter_file]</b></div>
<div style="float: left;"><button type="button" onclick="save(this.form);">SAVE</button></div>
<div style="float: left;"><button type="button" onclick="savenexit(this.form);">SAVE AND EXIT</button></div>
<div style="float: right;"><button type="button" onclick="exitnosave(this.form);">EXIT NO SAVE</button></div>
</div>
<div style="background-color: #abc; padding: 0px 2px 6px 2px;">
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="return do_undo();"><img width="18" src="/assets/htmleditor_undo.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="return do_redo();"><img width="18" src="/assets/htmleditor_redo.png"></div>
&nbsp;
<div style="display: inline-block; float: left; margin: 2px 0px 0px 0px;">
<select style="border: 1pt solid #123; border-radius: 3px; background-color: #def;" onchange="document.execCommand('formatBlock',false,this.value);return true;">
<option value="p">Paragraph</option>
<option value="h1">Heading 1</option>
<option value="h2">Heading 2</option>
<option value="h3">Heading 3</option>
<option value="pre">Monospace</option>
</select>
</div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 5px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('bold');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_bold.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('underline');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_underline.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 5px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('justifyLeft');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_jl.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('justifyCenter');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_jc.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('justifyRight');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_jr.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('justifyFull');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_jj.png"></div>
<div id="map" style="display: inline-block; color: #012; font-size: 10px;"></div>
</div>
<div id="mainpage"
style="width: 1000px; height: 800px; border: 1px solid black; position: relative; background: #ffffffff; overflow: scroll; max-height: 800px; max-width: 1000px; cursor: crosshair;"
onmousedown="return mousedown(event);"
onmousemove="return mousemove(event);"
onmouseup="return mouseup(event); return true;">
$[mhtml]
</div>
<canvas id="grid" width="1000px" height="800px" style="position: absolute; left: 0px; top: 0px; alpha: 0.5; cursor: normal; pointer-events: none;"></canvas>
</div>
<div id="window3" style="position:absolute; top: 235px; left: 1010px;
border: 1pt solid black; display: inline-block; padding: 2px; background: #808080; color: #ffffff;">
<span>Options</span>
<div style="width: 320px; color: #000000; border: 1px solid black; position: relative; background: #ffffff;">
<div id="controls">
<input type="checkbox" name="grid" onchange="showGrid = this.checked; drawGrid(); return true;"><span style="font-size: 12px; margin-bottom: 2px;">Show Grid</span><br>
<input type="checkbox" name="snaptogrid" onchange="snapToGrid = this.checked; return true;"><span style="font-size: 12px; margin-bottom: 2px;">Snap To Grid</span><br>
<span style="font-size: 12px; margin-bottom: 2px;">Grid Size: </span><input type="text" name="gridsize" id="gridsize" style="width: 20px;" size="3" onchange="gridSize = parseInt(this.value); drawGrid(); return true;"><br>
</div>
<div id="itemparameters" style="font-size: 12px;"></div>
<div id="data" style="font-size: 12px;">
<p>NO DATA</p>
</div>
</div>
</div>
<div id="window2" style="position:absolute; top: 0px; left: 1010px; border: 1pt solid black; display: inline-block; padding: 2px; background: #808080; color: #ffffff;">
<span>Toolbar</span>
<div id="toolbar" style="overflow-y: auto; width: 200px; height: 300px; border: 1px solid black; position: relative; background: #ffffff;">
</div>
</div>
<div id="window4" style="position:absolute; top: 860px; left: 0px;
border: 1pt solid black; display: inline-block; padding: 2px; background: #808080; color: #ffffff;">
<span>Images</span>
<div style="width: 1000px; height: 300px; color: #000000; border: 1px solid black; position: relative; background: #ffffff;">
<div id="controls" style="overflow-y: scroll; max-height: 275px; padding: 3px;">
<div style="display: inline-block;">
<div style="width: 100px; height: 100px; border: 1px solid #000; margin: 5px 0px 5px 5px; background-image: url('/assets/htmleditor_drinkwise_logo.png'); background-size: contain; background-repeat: no-repeat; cursor: pointer;" onmousedown="newImage('htmleditor_drinkwise_logo.png', event); return true;"></div>
<div style="font-size: 8px;">drinkwise_logo.png</div>
</div>
<div style="display: inline-block;">
<div style="width: 100px; height: 100px; border: 1px solid #000; margin: 5px 0px 5px 5px; background-image: url('/assets/htmleditor_dwbackground.png'); background-size: contain; background-repeat: no-repeat; cursor: pointer;" onmousedown="newImage('htmleditor_dwbackground.png', event); return true;"></div>
<div style="font-size: 8px;">dwbackground.png</div>
</div>
</div>
</div>
<div id="itemparameters" style="font-size: 12px;"></div>
</div>
</div>
</form>
</body>
</if>
</contents>
</page>
+1 -1
View File
@@ -1,6 +1,6 @@
<sectionframe title="INVOICE HISTORY"> <sectionframe title="INVOICE HISTORY">
<sql>select * from invoices where user_id=$[_user_id]</sql>$[sqlerror] <sql>select * from invoices where user_id=$[_user_id]</sql>$[sqlerror]
<whilerow> <whilerow>
<linkbutton page="barantmail">|$[created]|</linkbutton><br> <linkbutton process="selectinvoice" page="account" name="invoice_id" value="$[.id]">|$[.created]|</linkbutton><br>
</whilerow> </whilerow>
</sectionframe> </sectionframe>
+50 -10
View File
@@ -27,17 +27,57 @@ to.</p>
many expression handling functions are dependant on the mysql library many expression handling functions are dependant on the mysql library
functions. Any expression that works in mysql will work in JET.</p> functions. Any expression that works in mysql will work in JET.</p>
<h3>JET Libraries</h3> <h3>One Language, Two Speeds: The JET 2.0 Interpreter and Compiler</h3>
<p>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.</p>
<p>Libraries allow you to make your own tags and implement them in <p>Every JET script runs two ways, and you never have to choose in
the scripts. JET does not have JSON or XML parsing but using a advance. Drop a file on disk and JET interprets it on the spot &mdash;
libraries you can develop an XML parser operation that usually edit it, refresh, see the change. No build step, no deploy pipeline,
suffices for most script processing. no waiting. That's the interpreter, and it's what makes JET feel less
like "compiling a website" and more like sketching one.</p>
<p>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, <b>jetc</b>. Not bytecode. Not a virtual machine sitting
in the middle slowing things down. Real, compiled C++ &mdash; the
tags you wrote resolve once, at build time, instead of being
re-parsed on every single request.</p>
<p>That's the pitch in one line: write it fast, then make it fast.</p>
<p><b>What jetc gives you:</b></p>
<ul>
<li><b>Genuine compiled-code speed.</b> A compiled JET module isn't
an interpreted script pretending to be quick &mdash; 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.</li>
<li><b>Zero-downtime deploys, by design.</b> 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.</li>
<li><b>Routes that declare themselves.</b> A compiled module carries
its own API routes right inside the source file. Drop it in the
plugin directory and it's live &mdash; there's no central routing
table to hand-edit and no separate registration step to forget.</li>
<li><b>Builds that only do the work that changed.</b> 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.</li>
<li><b>Shared code costs nothing at runtime.</b> Common tag
definitions and includes get inlined directly into the compiled
output at build time &mdash; you write clean, reusable JET source,
and the compiler makes sure reusing it is free.</li>
</ul>
<p>The interpreter and the compiler aren't two different products
you have to pick between &mdash; 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.</p>
<h3>Session Control (SESSIONCTL) Library</h3> <h3>Session Control (SESSIONCTL) Library</h3>
<p>For web sites, a session control library is provided with JET and <p>For web sites, a session control library is provided with JET and
+16
View File
@@ -0,0 +1,16 @@
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<sessionctl page="login">
<defaultnav>
<setpage name="login">
</defaultnav>
<link rel="stylesheet" href="/assets/main.css">
<link rel="stylesheet" href="/assets/control.css">
</sessionctl>
</database>
+1 -1
View File
@@ -1,5 +1,5 @@
<if value1="$[_login_id]" value2="" type="ne"> <if value1="$[_login_id]" value2="" type="ne">
<div style="color: #444; margin-bottom: 3px;">Welcome $[_login_name]</div> <div class="user-badge">Welcome $[_login_name]</div>
<div> <div>
<linkbutton page="cart"><img style="margin-top: 0px; margin-right: 10px;" src="/assets/shopping_cart.png" width="30px" align="center"></linkbutton> <linkbutton page="cart"><img style="margin-top: 0px; margin-right: 10px;" src="/assets/shopping_cart.png" width="30px" align="center"></linkbutton>
<linkbutton process="logout" class="button">Logout</linkbutton> <linkbutton process="logout" class="button">Logout</linkbutton>
+7
View File
@@ -0,0 +1,7 @@
<sectionframe title="QUICK LINKS">
<linkbutton page="services">SERVICES</linkbutton><br>
<linkbutton page="hosting">HOSTING</linkbutton><br>
<linkbutton page="siptrunk">SIP TRUNKING</linkbutton><br>
<linkbutton page="opensource">OPEN SOURCE</linkbutton><br>
<linkbutton page="contact">CONTACT</linkbutton>
</sectionframe>
+149
View File
@@ -0,0 +1,149 @@
#api GET /
#api GET /{page}
#api GET /{page}/{subpage}
#api POST /
#api POST /{page}
#api POST /{page}/{subpage}
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<if value1="$[PROCESS]" value2="" type="ne">
<tag name="process" scope="local"><eval><container></eval></tag>
<eval name="_procout"><include file="./$[DIR]/$[PROCESS].proc"></eval>
</if>
<checksession>
<if value1="$[PAGE]" value2="" type="eq">
<if value1="$[page]" value2="" type="eq">
<set name="PAGE" value="main">
<else>
<set name="PAGE" value="$[page]">
</if>
</if>
<if value1="$[DIR]" value2="" type="eq">
<set name="DIR" value="">
</if>
<if value1="$[SUBPAGE]" value2="" type="eq">
<if value1="$[subpage]" value2="" type="ne">
<set name="SUBPAGE" value="$[subpage]">
</if>
</if>
<tag name="page" keywords="class onload secure loggedin admin" scope="local">
<tag name="contents" scope="local"><eval name="_BODY"><container></eval></tag>
<eval name="_leftover"><container></eval>
<if expr="('$[HTTPS]'!='on') and ('#[secure]'='yes')">
<if value1="$[_fragment]" value2="" type="eq">
<html><head><meta http-equiv="refresh" content="0; url=https://$[HTTP_HOST]$[REQUEST_URI]"></head></html>
<else>
<div data-spa-redirect="https://$[HTTP_HOST]$[REQUEST_URI]"></div>
</if>
<else>
<if expr="('$[_login_type]'='user') and ('#[admin]'='true')">
<p>Page not found.</p>
<else>
<if expr="('$[_login_id]'='') and ('#[loggedin]'='yes')">
<if value1="$[_fragment]" value2="" type="eq">
<html><head><meta http-equiv="refresh" content="0; url=https://$[HTTP_HOST]/login?start=$[PAGE]"></head></html>
<else>
<div data-spa-redirect="/login?start=$[PAGE]"></div>
</if>
<else>
<if expr="('$[_login_type]'='user') and ('#[admin]'='yes')">
<p>Page not found.</p>
<else>
$[_BODY]
</if>
</if>
</if>
</if>
</tag>
<if value1="$[_fragment]" value2="" type="eq">
<!DOCTYPE html>
<html lang="en">
<head>
<title>BARANT Technology Group</title>
<link rel="stylesheet" href="/assets/main.css?v=20260906a">
<link rel="stylesheet" href="/assets/control.css?v=20260902">
</head>
<body>
<form id="mainform" method="post" action="/$[PAGE]<if value1="$[SUBPAGE]" value2="" type="ne">/$[SUBPAGE]</if>" enctype="multipart/form-data">
<input type="hidden" name="_LASTPAGE" value="$[PAGE]">
<input type="hidden" name="PROCESS" value="">
<input type="hidden" name="EXITPROCESS" value="">
<input type="hidden" name="DIR" value="">
<input type="hidden" name="_CTLDIR" value="">
<input type="hidden" name="PAGE" value="$[PAGE]">
<input type="hidden" name="SUBPAGE" value="$[SUBPAGE]">
<input type="hidden" name="_TIMESTAMP" value="">
<div class="pageclass">
<div class="heading">
<div style="display: inline-block; width: 500px;">
<a href="https://barant.com"><img style="width: 60px; height: 60px; margin: 10px; background-color: #000; border-radius: 6px; padding: 4px; box-sizing: border-box;" align="left" src="/assets/btglogo.png"></a>
<div style="padding-top: 15px;">
<div style="left: 80px; color: #c5a45a; font-size: 20pt; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 600;">BARANT TECHNOLOGY GROUP</div>
</div>
</div>
<div id="loginmenu" style="float: right; text-align: right; margin-right: 10px; margin-top: 5px;"><include file="login.menu"></div>
</div>
<div class="menubar" id="menubar">
<include file="menu.bar">
</div>
<div style="display: flex; flex: 1; width: 100%; min-height: 0;">
<div style="display: flex; width: 100%;">
<div id="panelmenu" class="panel" style="width: 200px; flex-shrink: 0;">
<try><include file="$[PAGE].menu"><catch></try>
<include file="cart.menu">
</div>
<div id="panelmenu-resizer" title="Drag to resize"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5 L3 12 L8 19"/><path d="M16 5 L21 12 L16 19"/></svg></div>
<div class="panel" id="content" style="flex: 1; min-width: 0;">
<try>
<eval><include file="./$[DIR]/$[PAGE].page"></eval>
<catch>
<p>Page not found.</p>
</try>
</div>
<div style="clear: both;"></div>
</div>
</div>
<div class="footer" style="clear: both;">&copy; 1999-2026 BARANT Systems, All Rights Reserved</div>
</div>
</form>
<div id="filemgr-panel" class="filemgr-collapsed">
<div id="filemgr-toggle" title="File Transfers">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V6M5 12l7-7 7 7"/><path d="M5 21h14"/></svg>
<span id="filemgr-badge"></span>
</div>
<div id="filemgr-body">
<div id="filemgr-header">
<span>Transfers</span>
<span id="filemgr-clear" title="Clear completed">Clear</span>
</div>
<div id="filemgr-list"><div id="filemgr-empty">No transfers yet.</div></div>
</div>
</div>
<script src="/assets/spa.js?v=20260906a"></script>
<script src="/assets/documents.js?v=20260906a"></script>
</body>
</html>
<else>
<div data-spa-page="$[PAGE]" data-spa-subpage="$[SUBPAGE]" style="display:none"></div>
<div id="_spa_panelmenu" style="display:none"><try><include file="$[PAGE].menu"><catch></try><include file="cart.menu"></div>
<div id="_spa_menubar" style="display:none"><include file="menu.bar"></div>
<div id="_spa_loginmenu" style="display:none"><include file="login.menu"></div>
<try>
<eval><include file="./$[DIR]/$[PAGE].page"></eval>
<catch>
<p>Page not found.</p>
</try>
</if>
</database>
+2 -4
View File
@@ -10,6 +10,7 @@
<tab page="main"><a href="/main">HOME</a></tab> <tab page="main"><a href="/main">HOME</a></tab>
<tab page="services"><a href="/services">SERVICES</a></tab> <tab page="services"><a href="/services">SERVICES</a></tab>
<tab page="hosting"><a href="/hosting">HOSTING</a></tab> <tab page="hosting"><a href="/hosting">HOSTING</a></tab>
<tab page="store"><a href="/store/createstore">CREATE STORE</a></tab>
<tab page="documents"><a href="/documents">DOCUMENT MGMT</a></tab> <tab page="documents"><a href="/documents">DOCUMENT MGMT</a></tab>
<tab page="siptrunk"><a href="/siptrunk">SIP TRUNKING</a></tab> <tab page="siptrunk"><a href="/siptrunk">SIP TRUNKING</a></tab>
<tab page="vst"><a href="/vst">VST</a></tab> <tab page="vst"><a href="/vst">VST</a></tab>
@@ -26,10 +27,7 @@
</if> </if>
<if value1="$[_login_id]" value2="" type="ne"> <if value1="$[_login_id]" value2="" type="ne">
<tab page="account"><a href="/account">ACCOUNT</a></tab> <tab page="account"><a href="/account">ACCOUNT</a></tab>
<tab page="email"><a href="/email">EMAIL</a></tab> <tab page="admin"><a href="/admin">ADMIN</a></tab>
<if value1="$[_login_type]" value2="admin" type="eq">
<tab page="admin"><a href="/admin">ADMIN</a></tab>
</if>
<else> <else>
<tab page="login"><a href="/login">LOGIN</a></tab> <tab page="login"><a href="/login">LOGIN</a></tab>
</if> </if>
+27
View File
@@ -0,0 +1,27 @@
<page>
<title>LOADED MODULES</title>
<contents>
<sectionframe title="LIVE JTC MODULES">
<p>This APIServer process is running as a small core loader with the
real functionality split into separately compiled <b>.jtc</b> modules.
Each one is dlopen()'d straight off disk by ApiModuleWatcher, and
watched live &mdash; drop a new build into the plugin directory and it's
serving requests within moments, with zero downtime and no restart.</p>
<p>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.</p>
<table>
<tr><th>Module</th><th>File</th></tr>
<whiledir dir="/home/barant/servercore-analysis/APIServer/plugins-barant-site" name="filename" filenamenoextension="modname" sort="true">
<if expr="right('$[filename]',4) = '.jtc'">
<tr><td>$[modname]</td><td>$[filename]</td></tr>
</if>
</whiledir>
</table>
</sectionframe>
</contents>
</page>
@@ -1,6 +1,3 @@
<page>
<title>RCA1802 Micro-Controller Assembler/Emulator</title>
<contents>
<sectionframe title="RCA1802"> <sectionframe title="RCA1802">
<h1>The RCA1802 Micro-Controller</h1> <h1>The RCA1802 Micro-Controller</h1>
<img src="/assets/rca1802chip.jpeg" align="right"> <img src="/assets/rca1802chip.jpeg" align="right">
@@ -47,6 +44,3 @@ 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 change environment going for the Kicad project as well as the source
code for the assembler and emulator components. code for the assembler and emulator components.
</sectionframe> </sectionframe>
</contents>
</page>
@@ -0,0 +1,53 @@
<sectionframe title="BARANTMAIL OPEN SOURCE PROJECT">
<p>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.</p>
<p>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.</p>
<p>The result is BARANTMAIL.</p>
<h2>Features</h2>
<p>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.</p>
<h3>Protocols</h3>
<ul>
<li>SMTP (port 25) plus authenticated SMTP-over-TLS submission (port 465).</li>
<li>IMAP4rev1 over TLS (port 993) - LOGIN, SELECT, FETCH/UID FETCH, STORE/UID STORE, COPY/UID COPY, EXPUNGE, SEARCH/UID SEARCH, SUBSCRIBE, NOOP and more.</li>
<li>POP3 over TLS (port 995) - USER/PASS, STAT, LIST, RETR, DELE, TOP, UIDL.</li>
</ul>
<h3>Security</h3>
<ul>
<li>TLS/SSL on every protocol, backed by OpenSSL and real CA-issued certificates.</li>
<li>DKIM signing on outbound mail.</li>
<li>SPF verification on inbound mail.</li>
</ul>
<h3>Spam Filtering</h3>
<ul>
<li>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.</li>
<li>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.</li>
</ul>
<h3>Multi-Tenancy</h3>
<ul>
<li>Multiple domains on one server (virtual hosting).</li>
<li>Multiple independent mailboxes per domain, each with its own Inbox, Spam, and Trash folders, plus support for any additional folder a client creates.</li>
</ul>
<h3>Architecture</h3>
<ul>
<li>Asynchronous, EPoll-based I/O - handles many concurrent connections without a thread per connection.</li>
<li>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.</li>
</ul>
<h3>Companion Webmail Client</h3>
<p>A full browser-based email client (see <a href="/admin/email">Email</a>
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.</p>
</sectionframe>
@@ -1,13 +1,6 @@
<page>
<title>barant.com - JET Extension Tags</title>
<contents>
<if value1="$[ARGS]" value2="" type="eq"> <if value1="$[ARGS]" value2="" type="eq">
<include file="jet_overview.section"> <include file="jet_overview.section">
<else> <else>
<set name="subpage" expr="substring('$[ARGS]', 2)"> <set name="subpage" expr="substring('$[ARGS]', 2)">
<include file="jet_$[subpage].section"> <include file="jet_$[subpage].section">
</if> </if>
</contents>
</page>
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more