#include "common/Email.h" #include #include #include #include #include #include #include namespace visione::email { namespace { const Json::Value &mailConfig() { return drogon::app().getCustomConfig()["mail"]; } std::string configString(const std::string &key, const std::string &fallback) { const auto &mail = mailConfig(); return mail.get(key, fallback).asString(); } int configInt(const std::string &key, int fallback) { const auto &mail = mailConfig(); return mail.get(key, fallback).asInt(); } bool configBool(const std::string &key, bool fallback) { const auto &mail = mailConfig(); return mail.get(key, fallback).asBool(); } struct SmtpDefaults { std::string host; int port; std::string username; bool useTls; bool useStartTls; bool verifyPeer; bool verifyHost; }; SmtpDefaults smtpDefaults(const std::string &provider) { if (provider == "resend") { return { .host = "smtp.resend.com", .port = 465, .username = "resend", .useTls = true, .useStartTls = false, .verifyPeer = true, .verifyHost = true, }; } return { .host = "127.0.0.1", .port = 1025, .username = "", .useTls = false, .useStartTls = false, .verifyPeer = false, .verifyHost = false, }; } std::string smtpUrl(const std::string &host, int port, bool useTls) { return (useTls ? "smtps://" : "smtp://") + host + ":" + std::to_string(port); } struct UploadContext { const std::string *payload; size_t offset{0}; }; size_t readPayload(char *buffer, size_t size, size_t count, void *userData) { auto *context = static_cast(userData); const auto capacity = size * count; const auto remaining = context->payload->size() - context->offset; const auto bytesToCopy = std::min(capacity, remaining); if (bytesToCopy == 0) { return 0; } std::memcpy(buffer, context->payload->data() + static_cast(context->offset), bytesToCopy); context->offset += bytesToCopy; return bytesToCopy; } bool ensureCurlInitialized() { static const bool initialized = []() { const auto result = curl_global_init(CURL_GLOBAL_DEFAULT); if (result != CURLE_OK) { LOG_ERROR << "curl_global_init failed: " << curl_easy_strerror(result); return false; } return true; }(); return initialized; } } // namespace std::string EmailService::deliveryName() const { return configString("provider", "smtp"); } bool EmailService::sendOtp(const std::string &to, const std::string &code) const { const auto provider = deliveryName(); const auto defaults = smtpDefaults(provider); const auto host = configString("host", defaults.host); const auto port = configInt("port", defaults.port); const auto username = configString("username", defaults.username); const auto password = configString("password", ""); const auto from = configString("from", ""); const auto useTls = configBool("use_tls", defaults.useTls); const auto useStartTls = configBool("use_starttls", defaults.useStartTls); const auto verifyPeer = configBool("verify_peer", defaults.verifyPeer); const auto verifyHost = configBool("verify_host", defaults.verifyHost); if (host.empty() || from.empty()) { LOG_ERROR << "Email delivery configuration missing: host or from"; return false; } if (provider == "resend" && password.empty()) { LOG_ERROR << "Email delivery configuration missing: SMTP password for Resend"; return false; } if (!ensureCurlInitialized()) { return false; } std::ostringstream payload; payload << "To: <" << to << ">\r\n"; payload << "From: Visione <" << from << ">\r\n"; payload << "Subject: Codice OTP Visione\r\n"; payload << "MIME-Version: 1.0\r\n"; payload << "Content-Type: text/plain; charset=utf-8\r\n"; payload << "\r\n"; payload << "Il tuo codice OTP per Visione e': " << code << "\r\n"; payload << "Scade tra 10 minuti.\r\n"; const auto payloadString = payload.str(); const auto url = smtpUrl(host, port, useTls); const auto mailFrom = "<" + from + ">"; const auto recipientAddress = "<" + to + ">"; using CurlHandle = std::unique_ptr; using CurlList = std::unique_ptr; CurlHandle curl(curl_easy_init(), &curl_easy_cleanup); if (!curl) { LOG_ERROR << "Could not initialize libcurl SMTP handle"; return false; } CurlList recipients(nullptr, &curl_slist_free_all); recipients.reset(curl_slist_append(nullptr, recipientAddress.c_str())); if (!recipients) { LOG_ERROR << "Could not build SMTP recipient list for " << to; return false; } UploadContext uploadContext{.payload = &payloadString}; char errorBuffer[CURL_ERROR_SIZE] = {0}; curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); curl_easy_setopt(curl.get(), CURLOPT_MAIL_FROM, mailFrom.c_str()); curl_easy_setopt(curl.get(), CURLOPT_MAIL_RCPT, recipients.get()); curl_easy_setopt(curl.get(), CURLOPT_READFUNCTION, readPayload); curl_easy_setopt(curl.get(), CURLOPT_READDATA, &uploadContext); curl_easy_setopt(curl.get(), CURLOPT_UPLOAD, 1L); curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, "visione-backend/1.0"); curl_easy_setopt(curl.get(), CURLOPT_CONNECTTIMEOUT, 10L); curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 20L); curl_easy_setopt(curl.get(), CURLOPT_ERRORBUFFER, errorBuffer); curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, verifyPeer ? 1L : 0L); curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, verifyHost ? 2L : 0L); if (!username.empty()) { curl_easy_setopt(curl.get(), CURLOPT_USERNAME, username.c_str()); } if (!password.empty()) { curl_easy_setopt(curl.get(), CURLOPT_PASSWORD, password.c_str()); } if (useStartTls) { curl_easy_setopt(curl.get(), CURLOPT_USE_SSL, CURLUSESSL_ALL); } const auto result = curl_easy_perform(curl.get()); if (result != CURLE_OK) { LOG_ERROR << "OTP email delivery failed via SMTP for " << to << ", provider=" << provider << ", host=" << host << ", port=" << port << ", error=" << (errorBuffer[0] != '\0' ? errorBuffer : curl_easy_strerror(result)); return false; } LOG_INFO << "OTP email sent via SMTP for " << to << ", provider=" << provider << ", host=" << host << ", port=" << port; return true; } } // namespace visione::email