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>
244 lines
8.2 KiB
C++
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}"));
|
|
}
|
|
|
|
}
|