Files
JetCore/__smtpsend.cpp
Brad ArantandClaude Sonnet 5 b8f7793773 Add global music player, invoicing/payment system, account subpages, mail account admin, and the qualityiron.vegas Jet 2.0 port
Jet interpreter (Tag.cpp, __call.cpp, __imap*.cpp/h, __smtpsend.cpp/h,
__imapemptyfolder.cpp/h - new): assorted parser/tag fixes found across
this work - a stray literal-tag-in-comment and backslash-quote parser
corruption class, a dangling-pointer fix, a __call.cpp fd leak, IMAP
folder-management tags, multi-account/per-host IMAP support, and a
smtpsend Message-ID fix.

barant.com site (jetc/barant-site/):
- A site-wide persistent music player (header band, playlist/queue,
  stereo FFT EQ, session-persisted state) fed by new library/
  ianslibrary/music song-list subpages.
- /account split into info/phones/invoices subpages; a full invoicing
  and Square payment system (create/edit/copy/pay/email invoice);
  admin invoice management and mail-account management subpages.
- /services split into overview/hosting/siptrunking/documents
  subpages, retiring the old standalone pages.
- SIPPHONE.tag: a Jet 2.0 WebRTC softphone tag.
- main.css/spa.js/email.js: the SPA loading indicator, the music
  player band styling, and assorted fixes accumulated alongside this
  work.

qualityiron-site/: the qualityiron.vegas job/contractor/customer/
document management app, ported from its original Jet 1.4 codebase to
Jet 2.0 as barant.coms APIServers second multi-tenant site.

wcwc/wcwc.jet: the WCWC site Jet source (wcwc.jtc build output not
included).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 17:42:01 +00:00

244 lines
8.2 KiB
C++

#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>
#include <iomanip>
#include <sstream>
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);
}
// RFC 5322 3.6.4: Message-ID "SHOULD be present" - in practice
// several real receivers (confirmed live: Gmail) now hard-reject
// anything missing one ("550 5.7.1 Messages missing a valid
// Message-ID header are not accepted") rather than just marking it
// down, so this isn't optional in practice. Domain part is the
// sender's own domain (everything after '@' in `from`), matching
// RFC 5322's own recommendation that it be a name under the
// generating system's control - falls back to the literal `from`
// string in the (should-never-happen) case it has no '@' at all.
std::string __smtpsend::generateMessageId(const std::string &from) {
size_t at = from.find('@');
std::string domainPart = (at == std::string::npos) ? from : from.substr(at + 1);
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
std::ostringstream idStream;
idStream << ts.tv_sec << "." << std::setfill('0') << std::setw(9) << ts.tv_nsec
<< "." << rand() << "@" << domainPart;
return idStream.str();
}
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 += "Message-ID: <" + generateMessageId(from) + ">\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}"));
}
}