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:
1 parent
b0cadc3f08
commit
c1491b044b
115 files changed
+6174
-137
No files matched your search
+220
@@ -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}"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user