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.
No files matched your search
@@ -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__/
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
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();
|
||||||
|
})();
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -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…">' +
|
||||||
|
'<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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
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 = '<' + curelem.nodeName + '>' + map;
|
||||||
|
curelem = curelem.parentNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('map').innerHTML = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jetEscape(text) {
|
||||||
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildToolbar() {
|
||||||
|
|
||||||
|
let tbitems = [
|
||||||
|
{
|
||||||
|
"label": "<div>",
|
||||||
|
"html": "<div id=\"newitem\" contenteditable=\"false\" style=\"width: 300px; height: 200px; background-color: #789;\"></div>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "<input>",
|
||||||
|
"html": "<input id=\"newitem\" type=\"text\">"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "<button>",
|
||||||
|
"html": "<button id=\"newitem\" type=\"button\">Label</button>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "<password>",
|
||||||
|
"html": "<input id=\"newitem\" type=\"password\">"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "<checkbox>",
|
||||||
|
"html": "<input id=\"newitem\" type=\"checkbox\">"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "<label>",
|
||||||
|
"html": "<label style=\"color: #000f;\" id=\"newitem\">Label text</label>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "<p>",
|
||||||
|
"html": "<p style=\"color: #000f;\" id=\"newitem\">Type Here</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "<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: <set>",
|
||||||
|
"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: <if>",
|
||||||
|
"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();
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 488 B |
|
After Width: | Height: | Size: 457 B |
|
After Width: | Height: | Size: 470 B |
|
After Width: | Height: | Size: 492 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -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; }
|
||||||
|
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.';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 99 KiB |
@@ -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.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 55 KiB |
@@ -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&file=%s">%s/%s</a></div>\n' "$d" "$n" "$d" "$n"
|
||||||
|
done
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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…">
|
||||||
|
<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">•</button>
|
||||||
|
<button type="button" data-cmd="createLink" title="Link">🔗</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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
@@ -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">
|
||||||
@@ -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>
|
||||||
@@ -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…</div></div>
|
||||||
|
</sectionframe>
|
||||||
|
<script src="/assets/documents-page.js?v=20260906a"></script>
|
||||||
|
</if>
|
||||||
</contents>
|
</contents>
|
||||||
</page>
|
</page>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
<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,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>
|
||||||
@@ -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 —
|
||||||
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++ — 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 — 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 — 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 — 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 — 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
|
||||||
|
|||||||
@@ -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,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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;">© 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 — 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>
|
|
||||||