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

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

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

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

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

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

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

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

No files matched your search

+220
View File
@@ -0,0 +1,220 @@
#include "__smtpsend.h"
#include "Exception.h"
#include "JFile.h"
#include "Base64.h"
#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <ctime>
namespace jet {
void __smtpsend::sendLine(const std::string &line) {
std::string data = line + "\r\n";
size_t sent = 0;
while (sent < data.size()) {
int n = SSL_write(ssl, data.data() + sent, (int)(data.size() - sent));
if (n <= 0)
throw coreutils::Exception("smtpsend: write failed", __FILE__, __LINE__, 502);
sent += (size_t)n;
}
}
char __smtpsend::readByte() {
if (recvPos >= recvBuffer.size()) {
char buf[4096];
int n = SSL_read(ssl, buf, sizeof(buf));
if (n <= 0)
throw coreutils::Exception("smtpsend: read failed", __FILE__, __LINE__, 502);
recvBuffer.assign(buf, buf + n);
recvPos = 0;
}
return recvBuffer[recvPos++];
}
std::string __smtpsend::readLine() {
std::string line;
char c;
for (;;) {
c = readByte();
if (c == '\n')
break;
if (c != '\r')
line += c;
}
return line;
}
int __smtpsend::readReply(int expectedCode) {
int code = 0;
std::string line;
for (;;) {
line = readLine();
if (line.size() < 4)
throw coreutils::Exception("smtpsend: malformed reply: " + line, __FILE__, __LINE__, 502);
code = atoi(line.substr(0, 3).c_str());
if (line[3] == ' ')
break;
// else line[3] == '-': a continuation line, same code - keep reading.
}
if (code / 100 != expectedCode / 100)
throw coreutils::Exception("smtpsend: unexpected reply " + std::to_string(code) + ": " + line, __FILE__, __LINE__, 502);
return code;
}
void __smtpsend::closeConnection() {
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); ssl = nullptr; }
if (fd >= 0) { ::close(fd); fd = -1; }
if (ctx) { SSL_CTX_free(ctx); ctx = nullptr; }
}
std::string __smtpsend::base64Encode(const std::string &s) {
coreutils::Base64 base64;
std::string copy = s;
coreutils::ZString z(copy);
return base64.encode(z).str();
}
std::string __smtpsend::rfc2822Date() {
time_t now = time(nullptr);
struct tm tmResult{};
gmtime_r(&now, &tmResult);
char buf[64];
strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S +0000", &tmResult);
return std::string(buf);
}
std::string __smtpsend::buildMessage(const std::string &from, const std::string &to,
const std::string &subject, const std::string &html) {
std::string msg;
msg += "Date: " + rfc2822Date() + "\r\n";
msg += "From: " + from + "\r\n";
msg += "To: " + to + "\r\n";
msg += "Subject: " + subject + "\r\n";
msg += "MIME-Version: 1.0\r\n";
msg += "Content-Type: text/html; charset=UTF-8\r\n";
msg += "Content-Transfer-Encoding: 8bit\r\n";
msg += "\r\n";
// SMTP DATA's dot-stuffing rule: any body line starting with '.'
// needs a second '.' prepended, or the server treats it as the
// end-of-data marker prematurely and truncates the message.
std::string stuffed;
stuffed.reserve(html.size());
bool atLineStart = true;
for (size_t i = 0; i < html.size(); i++) {
char c = html[i];
if (atLineStart && c == '.')
stuffed += '.';
stuffed += c;
atLineStart = (c == '\n');
}
msg += stuffed;
return msg;
}
__smtpsend::__smtpsend(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
std::string user = resolveKeyword("user").str();
std::string password = resolveKeyword("password").str();
std::string to = resolveKeyword("to").str();
std::string subject = resolveKeyword("subject").str();
std::string html = resolveKeyword("html").str();
coreutils::MString configPath = resolveKeyword("config");
if (user.empty() || password.empty())
throw coreutils::Exception("smtpsend: user/password must be specified.", __FILE__, __LINE__, 401);
if (to.empty())
throw coreutils::Exception("smtpsend: to address must be specified.", __FILE__, __LINE__, 400);
coreutils::JFile config(configPath);
std::string host = config.getString("host");
long port = (long)config.getNumber("smtpPort");
if (port <= 0)
port = 465;
struct addrinfo hints {};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo *result = nullptr;
std::string portStr = std::to_string(port);
if (getaddrinfo(host.c_str(), portStr.c_str(), &hints, &result) != 0 || !result)
throw coreutils::Exception("smtpsend: could not resolve " + host, __FILE__, __LINE__, 502);
int newFd = -1;
for (struct addrinfo *rp = result; rp != nullptr; rp = rp->ai_next) {
newFd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (newFd < 0)
continue;
if (::connect(newFd, rp->ai_addr, rp->ai_addrlen) == 0)
break;
::close(newFd);
newFd = -1;
}
freeaddrinfo(result);
if (newFd < 0)
throw coreutils::Exception("smtpsend: could not connect to " + host, __FILE__, __LINE__, 502);
struct timeval timeout { 30, 0 };
setsockopt(newFd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
setsockopt(newFd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
SSL_CTX *newCtx = SSL_CTX_new(SSLv23_client_method());
if (!newCtx) {
::close(newFd);
throw coreutils::Exception("smtpsend: SSL_CTX_new failed", __FILE__, __LINE__, 502);
}
SSL *newSsl = SSL_new(newCtx);
SSL_set_fd(newSsl, newFd);
if (SSL_connect(newSsl) != 1) {
SSL_free(newSsl);
SSL_CTX_free(newCtx);
::close(newFd);
throw coreutils::Exception("smtpsend: TLS handshake failed", __FILE__, __LINE__, 502);
}
ctx = newCtx;
ssl = newSsl;
fd = newFd;
try {
readReply(220);
sendLine("EHLO " + host);
readReply(250);
sendLine("AUTH LOGIN");
readReply(334);
sendLine(base64Encode(user));
readReply(334);
sendLine(base64Encode(password));
readReply(235);
sendLine("MAIL FROM:<" + user + ">");
readReply(250);
sendLine("RCPT TO:<" + to + ">");
readReply(250);
sendLine("DATA");
readReply(354);
std::string message = buildMessage(user, to, subject, html);
sendLine(message);
sendLine(".");
readReply(250);
sendLine("QUIT");
readReply(221);
} catch (...) {
closeConnection();
throw;
}
closeConnection();
out = coreutils::MString(std::string("{\"ok\":true}"));
}
}