Skip to content

Instantly share code, notes, and snippets.

@aneury1
Created March 4, 2026 18:49
Show Gist options
  • Select an option

  • Save aneury1/d4dc68ff2fad98b700c79b1a8ff2d269 to your computer and use it in GitHub Desktop.

Select an option

Save aneury1/d4dc68ff2fad98b700c79b1a8ff2d269 to your computer and use it in GitHub Desktop.
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <cstring>
#include <ctime>
#include <iostream>
#include <fstream>
#include <atomic>
#include <string>
// TLS
#include <openssl/ssl.h>
#include <openssl/err.h>
#define BUFFER_SIZE 4096
// --------------------------------------------------------------------------
// TLS globals — loaded once at startup
// --------------------------------------------------------------------------
static SSL_CTX* g_ssl_ctx = nullptr;
bool initTLS(const std::string& cert_file, const std::string& key_file) {
SSL_library_init();
SSL_load_error_strings();
OpenSSL_add_all_algorithms();
g_ssl_ctx = SSL_CTX_new(TLS_server_method());
if (!g_ssl_ctx) {
std::cerr << "[TLS] SSL_CTX_new failed\n";
return false;
}
// Require TLS 1.2 or higher
SSL_CTX_set_min_proto_version(g_ssl_ctx, TLS1_2_VERSION);
SSL_CTX_set_options(g_ssl_ctx,
SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 |
SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 |
SSL_OP_CIPHER_SERVER_PREFERENCE);
if (SSL_CTX_use_certificate_chain_file(g_ssl_ctx, cert_file.c_str()) != 1) {
std::cerr << "[TLS] Failed to load certificate: " << cert_file << "\n";
ERR_print_errors_fp(stderr);
return false;
}
if (SSL_CTX_use_PrivateKey_file(g_ssl_ctx, key_file.c_str(), SSL_FILETYPE_PEM) != 1) {
std::cerr << "[TLS] Failed to load private key: " << key_file << "\n";
ERR_print_errors_fp(stderr);
return false;
}
if (SSL_CTX_check_private_key(g_ssl_ctx) != 1) {
std::cerr << "[TLS] Certificate and key do not match\n";
ERR_print_errors_fp(stderr);
return false;
}
std::cout << "[TLS] Context ready — cert: " << cert_file << "\n";
return true;
}
// --------------------------------------------------------------------------
// Logging
// --------------------------------------------------------------------------
void logMsg(const std::string& msg) {
std::cout << "[LOG] " << msg << "\n";
}
// --------------------------------------------------------------------------
// Send/recv helpers — transparent plain vs TLS
// --------------------------------------------------------------------------
void sendRaw(int fd, SSL* ssl, const std::string& msg) {
if (ssl)
SSL_write(ssl, msg.c_str(), (int)msg.size());
else
send(fd, msg.c_str(), msg.size(), 0);
}
void sendResponse(int fd, SSL* ssl, const std::string& msg) {
logMsg("SENT: " + msg.substr(0, msg.size() - 2));
sendRaw(fd, ssl, msg);
}
int recvData(int fd, SSL* ssl, char* buf, int len) {
if (ssl) return SSL_read(ssl, buf, len);
return (int)recv(fd, buf, len, 0);
}
// --------------------------------------------------------------------------
// Mail storage — saved in the same directory as the binary (./)
// --------------------------------------------------------------------------
std::string generateFilename() {
static std::atomic<unsigned int> counter{0};
time_t now = time(nullptr);
return "./" + std::to_string(now)
+ "_" + std::to_string(getpid())
+ "_" + std::to_string(counter++) + ".eml";
}
void saveMail(const std::string& data) {
std::string filename = generateFilename();
std::ofstream file(filename);
if (!file.is_open()) {
logMsg("ERROR: could not open " + filename);
return;
}
file << data;
file.close();
logMsg("Mail saved to: " + filename);
}
// --------------------------------------------------------------------------
// Core SMTP session — works on plain socket (ssl==nullptr) or TLS socket
// --------------------------------------------------------------------------
// send_banner=false after STARTTLS: RFC 3207 says no new 220 greeting
void smtpSession(int client, SSL* ssl, bool send_banner = true) {
if (send_banner)
sendResponse(client, ssl, "220 localhost ESMTP Ready\r\n");
std::string inputBuffer;
std::string mailData;
bool dataMode = false;
while (true) {
char buffer[BUFFER_SIZE];
int bytes = recvData(client, ssl, buffer, BUFFER_SIZE);
if (bytes <= 0) {
logMsg("Connection closed by client");
break;
}
inputBuffer.append(buffer, bytes);
size_t pos;
while ((pos = inputBuffer.find("\r\n")) != std::string::npos) {
std::string line = inputBuffer.substr(0, pos);
inputBuffer.erase(0, pos + 2);
logMsg("LINE: " + line);
if (dataMode) {
if (line == ".") {
logMsg("End of DATA detected");
saveMail(mailData);
mailData.clear();
dataMode = false;
sendResponse(client, ssl, "250 Message accepted\r\n");
} else {
// Dot-unstuffing per RFC 5321 §4.5.2
if (line.size() >= 2 && line[0] == '.' && line[1] == '.')
line = line.substr(1);
mailData += line + "\r\n";
}
continue;
}
if (line.rfind("EHLO", 0) == 0 || line.rfind("HELO", 0) == 0) {
if (!ssl && g_ssl_ctx) {
// Plain: offer STARTTLS, withhold AUTH until encrypted
sendRaw(client, nullptr, "250-localhost Hello\r\n");
sendRaw(client, nullptr, "250-SIZE 52428800\r\n");
sendRaw(client, nullptr, "250 STARTTLS\r\n");
} else {
// TLS active: advertise AUTH
sendRaw(client, ssl, "250-localhost Hello\r\n");
sendRaw(client, ssl, "250-SIZE 52428800\r\n");
sendRaw(client, ssl, "250 AUTH PLAIN LOGIN\r\n");
}
}
else if (line == "STARTTLS") {
if (ssl) {
// Already encrypted
sendResponse(client, ssl, "454 TLS already active\r\n");
} else if (!g_ssl_ctx) {
sendResponse(client, nullptr, "454 TLS not available — start server with cert+key\r\n");
} else {
sendRaw(client, nullptr, "220 Ready to start TLS\r\n");
logMsg("Upgrading connection to TLS via STARTTLS");
SSL* new_ssl = SSL_new(g_ssl_ctx);
SSL_set_fd(new_ssl, client);
if (SSL_accept(new_ssl) <= 0) {
ERR_print_errors_fp(stderr);
logMsg("STARTTLS handshake failed — closing connection");
SSL_free(new_ssl);
return;
}
logMsg("STARTTLS handshake complete");
// FIX: send_banner=false — no 220 after STARTTLS (RFC 3207)
// Client re-issues EHLO; server replies with AUTH in capability list
smtpSession(client, new_ssl, false);
SSL_shutdown(new_ssl);
SSL_free(new_ssl);
return; // outer plain session is done
}
}
else if (line.rfind("AUTH", 0) == 0) {
// AUTH PLAIN and AUTH LOGIN
// Accepts any non-empty credentials for local testing.
// Replace the accept condition with a real user lookup for production.
std::string method = (line.size() > 5) ? line.substr(5) : "";
while (!method.empty() && method.front() == ' ') method.erase(method.begin());
// Minimal base64 decoder
auto b64dec = [](const std::string& in) -> std::string {
static const char* T =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out; int val = 0, valb = -8;
for (unsigned char c : in) {
const char* p = strchr(T, c); if (!p){ valb-=6; continue; }
val=(val<<6)+(int)(p-T); valb+=6;
if(valb>=0){ out+=(char)((val>>valb)&0xFF); valb-=8; }
}
return out;
};
// Helper: read one line from the socket into a string
auto readLine = [&]() -> std::string {
std::string ln;
char c;
while (true) {
int n = recvData(client, ssl, &c, 1);
if (n <= 0) break;
if (c == '\n') { if (!ln.empty() && ln.back()=='\r') ln.pop_back(); break; }
ln += c;
}
return ln;
};
if (method.rfind("PLAIN", 0) == 0) {
// AUTH PLAIN base64 OR AUTH PLAIN (then credentials on next line)
std::string b64 = (method.size() > 6) ? method.substr(6) : "";
if (b64.empty()) {
sendRaw(client, ssl, "334 \r\n");
b64 = readLine();
}
std::string dec = b64dec(b64);
// Format: [authzid]\0authcid\0passwd
auto p1 = dec.find('\0');
auto p2 = (p1 != std::string::npos) ? dec.find('\0', p1+1) : std::string::npos;
if (p2 != std::string::npos) {
std::string user = dec.substr(p1+1, p2-p1-1);
std::string pass = dec.substr(p2+1);
logMsg("AUTH PLAIN user=[" + user + "]");
if (!user.empty() && !pass.empty())
sendResponse(client, ssl, "235 Authentication successful\r\n");
else
sendResponse(client, ssl, "535 Authentication failed\r\n");
} else {
sendResponse(client, ssl, "535 Authentication failed\r\n");
}
} else if (method.rfind("LOGIN", 0) == 0) {
sendRaw(client, ssl, "334 VXNlcm5hbWU6\r\n"); // "Username:" in base64
std::string user = b64dec(readLine());
sendRaw(client, ssl, "334 UGFzc3dvcmQ6\r\n"); // "Password:" in base64
std::string pass = b64dec(readLine());
logMsg("AUTH LOGIN user=[" + user + "]");
if (!user.empty() && !pass.empty())
sendResponse(client, ssl, "235 Authentication successful\r\n");
else
sendResponse(client, ssl, "535 Authentication failed\r\n");
} else {
sendResponse(client, ssl, "504 Unrecognized auth mechanism\r\n");
}
}
else if (line.rfind("MAIL FROM:", 0) == 0) {
sendResponse(client, ssl, "250 OK\r\n");
}
else if (line.rfind("RCPT TO:", 0) == 0) {
sendResponse(client, ssl, "250 OK\r\n");
}
else if (line == "DATA") {
sendResponse(client, ssl, "354 End data with <CR><LF>.<CR><LF>\r\n");
dataMode = true;
logMsg("Entering DATA mode");
}
else if (line == "QUIT") {
sendResponse(client, ssl, "221 Bye\r\n");
logMsg("QUIT received");
return;
}
else {
sendResponse(client, ssl, "500 Command not recognized\r\n");
}
}
}
}
// --------------------------------------------------------------------------
// handleClient:
// implicit_tls = false → plain socket, STARTTLS offered after EHLO
// implicit_tls = true → TLS handshake immediately (SMTPS / port 465)
// --------------------------------------------------------------------------
void handleClient(int client, bool implicit_tls) {
logMsg("New connection (mode: " +
std::string(implicit_tls ? "implicit TLS" : "plain + STARTTLS") + ")");
if (implicit_tls) {
if (!g_ssl_ctx) {
logMsg("Implicit TLS requested but no TLS context — closing");
close(client);
return;
}
SSL* ssl = SSL_new(g_ssl_ctx);
SSL_set_fd(ssl, client);
if (SSL_accept(ssl) <= 0) {
ERR_print_errors_fp(stderr);
logMsg("Implicit TLS handshake failed — closing");
SSL_free(ssl);
close(client);
return;
}
logMsg("Implicit TLS handshake complete");
smtpSession(client, ssl);
SSL_shutdown(ssl);
SSL_free(ssl);
} else {
smtpSession(client, nullptr);
}
close(client);
logMsg("Connection finished");
}
// --------------------------------------------------------------------------
// main
//
// Usage:
// ./smtp_server <port>
// Plain SMTP only (no TLS)
//
// ./smtp_server <port> <fullchain.pem> <privkey.pem>
// Plain SMTP on <port>, STARTTLS available after EHLO
// Example: ./smtp_server 587 /etc/letsencrypt/live/domain/fullchain.pem
// /etc/letsencrypt/live/domain/privkey.pem
//
// ./smtp_server <port> <fullchain.pem> <privkey.pem> tls
// Implicit TLS (SMTPS) — TLS from the very first byte
// Example: ./smtp_server 465 /etc/letsencrypt/live/domain/fullchain.pem
// /etc/letsencrypt/live/domain/privkey.pem tls
// --------------------------------------------------------------------------
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage:\n"
<< " " << argv[0] << " <port>\n"
<< " " << argv[0] << " <port> <fullchain.pem> <privkey.pem>\n"
<< " " << argv[0] << " <port> <fullchain.pem> <privkey.pem> tls\n";
return 1;
}
int port = std::stoi(argv[1]);
bool implicit_tls = false;
if (argc >= 4) {
if (!initTLS(argv[2], argv[3])) return 1;
if (argc >= 5 && std::string(argv[4]) == "tls")
implicit_tls = true;
}
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) { perror("socket"); return 1; }
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
if (bind(server_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
if (listen(server_fd, 10) < 0) { perror("listen"); return 1; }
logMsg("Listening on port " + std::to_string(port) +
(g_ssl_ctx ? (implicit_tls ? " [implicit TLS / SMTPS]"
: " [plain + STARTTLS]")
: " [plain — no TLS]"));
while (true) {
int client = accept(server_fd, nullptr, nullptr);
if (client < 0) continue;
handleClient(client, implicit_tls);
}
close(server_fd);
if (g_ssl_ctx) SSL_CTX_free(g_ssl_ctx);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment