#include "__smtpsend.h" #include "Exception.h" #include "JFile.h" #include "Base64.h" #include #include #include #include #include #include #include #include #include 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}")); } }