240 lines
6.9 KiB
C++
240 lines
6.9 KiB
C++
#include "common/Email.h"
|
|
|
|
#include <curl/curl.h>
|
|
#include <drogon/drogon.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <sstream>
|
|
|
|
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<UploadContext *>(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<std::ptrdiff_t>(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<CURL, decltype(&curl_easy_cleanup)>;
|
|
using CurlList = std::unique_ptr<curl_slist, decltype(&curl_slist_free_all)>;
|
|
|
|
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
|