Created
March 4, 2026 18:49
-
-
Save aneury1/d4dc68ff2fad98b700c79b1a8ff2d269 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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