v0.3.2: Add mail::SmtpTransport — lifted from fewo-webapp
Pure libcurl SMTP + MIME transport, DTO-free so it drops into any consumer that can cough up host/port/from/user/pass. Callers adapt their own settings row/DTO to `oatpp_authkit::mail::SmtpConfig`. Closes the email-service half of #447 (tracked under fewo-webapp #454). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5cdcb69edb
commit
448cd9ef8c
2 changed files with 151 additions and 1 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
cmake_minimum_required(VERSION 3.14)
|
cmake_minimum_required(VERSION 3.14)
|
||||||
project(oatpp-authkit VERSION 0.3.1 LANGUAGES CXX)
|
project(oatpp-authkit VERSION 0.3.2 LANGUAGES CXX)
|
||||||
|
|
||||||
# Header-only interface library — no compilation, just an include path and
|
# Header-only interface library — no compilation, just an include path and
|
||||||
# a CMake config package so consumers do:
|
# a CMake config package so consumers do:
|
||||||
|
|
|
||||||
150
include/oatpp-authkit/mail/SmtpTransport.hpp
Normal file
150
include/oatpp-authkit/mail/SmtpTransport.hpp
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
#ifndef oatpp_authkit_mail_SmtpTransport_hpp
|
||||||
|
#define oatpp_authkit_mail_SmtpTransport_hpp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file SmtpTransport.hpp
|
||||||
|
* @brief Pure libcurl SMTP+MIME transport — lifted from fewo-webapp #454.
|
||||||
|
*
|
||||||
|
* Handles MAIL FROM / RCPT TO / STARTTLS / optional SMTP AUTH / MIME multipart
|
||||||
|
* body + attachments / RFC 2047 encoded Subject. Knows nothing about templates,
|
||||||
|
* DTOs or databases — callers hand over the fully-rendered HTML body, the
|
||||||
|
* subject line, any attachment blobs and an `SmtpConfig` struct. Use a tiny
|
||||||
|
* adapter in the caller to map from whatever DTO/settings row you have to
|
||||||
|
* `SmtpConfig` so this header stays free of project-specific types.
|
||||||
|
*
|
||||||
|
* Consumers: add `#include <oatpp-authkit/mail/SmtpTransport.hpp>`; link
|
||||||
|
* against libcurl (authkit itself is header-only so the consumer's CMake
|
||||||
|
* owns the curl dependency).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace oatpp_authkit::mail {
|
||||||
|
|
||||||
|
/** @brief Plain-struct SMTP config; projects adapt from their own DTO/settings row. */
|
||||||
|
struct SmtpConfig {
|
||||||
|
std::string host;
|
||||||
|
int port = 587;
|
||||||
|
std::string fromAddress;
|
||||||
|
std::string username; // empty = no SMTP AUTH
|
||||||
|
std::string password;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @brief RFC 4648 Base64 encode — used for RFC 2047 Subject headers. */
|
||||||
|
inline std::string base64Encode(const std::string& data) {
|
||||||
|
static const char* table =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
std::string out;
|
||||||
|
out.reserve(((data.size() + 2) / 3) * 4);
|
||||||
|
for (size_t i = 0; i < data.size(); i += 3) {
|
||||||
|
unsigned int b = (unsigned char)data[i] << 16;
|
||||||
|
if (i + 1 < data.size()) b |= (unsigned char)data[i + 1] << 8;
|
||||||
|
if (i + 2 < data.size()) b |= (unsigned char)data[i + 2];
|
||||||
|
out += table[(b >> 18) & 0x3f];
|
||||||
|
out += table[(b >> 12) & 0x3f];
|
||||||
|
out += (i + 1 < data.size()) ? table[(b >> 6) & 0x3f] : '=';
|
||||||
|
out += (i + 2 < data.size()) ? table[b & 0x3f] : '=';
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a single email via libcurl SMTP.
|
||||||
|
*
|
||||||
|
* @param to Recipient address.
|
||||||
|
* @param subject Plain UTF-8 subject; wrapped as an RFC 2047 encoded-word so
|
||||||
|
* non-ASCII characters (umlauts etc.) survive.
|
||||||
|
* @param htmlBody text/html body (quoted-printable on the wire).
|
||||||
|
* @param attachments (filename, blob) pairs; `.pdf`/`.ics` extensions get
|
||||||
|
* recognised Content-Type, everything else goes as
|
||||||
|
* application/octet-stream.
|
||||||
|
* @param cfg SMTP configuration.
|
||||||
|
* @return Empty string on success; error message otherwise. Callers typically
|
||||||
|
* log a non-empty result and treat it as a soft failure.
|
||||||
|
*/
|
||||||
|
inline std::string send(
|
||||||
|
const std::string& to,
|
||||||
|
const std::string& subject,
|
||||||
|
const std::string& htmlBody,
|
||||||
|
const std::vector<std::pair<std::string, std::string>>& attachments,
|
||||||
|
const SmtpConfig& cfg)
|
||||||
|
{
|
||||||
|
if (cfg.host.empty()) return "SMTP not configured (no host)";
|
||||||
|
if (cfg.fromAddress.empty()) return "SMTP not configured (no from_address)";
|
||||||
|
|
||||||
|
CURL* curl = curl_easy_init();
|
||||||
|
if (!curl) return "curl_easy_init failed";
|
||||||
|
|
||||||
|
std::string url = "smtp://" + cfg.host + ":" + std::to_string(cfg.port);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_MAIL_FROM, ("<" + cfg.fromAddress + ">").c_str());
|
||||||
|
|
||||||
|
struct curl_slist* rcpt = nullptr;
|
||||||
|
rcpt = curl_slist_append(rcpt, to.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, rcpt);
|
||||||
|
|
||||||
|
if (!cfg.username.empty()) {
|
||||||
|
curl_easy_setopt(curl, CURLOPT_USERNAME, cfg.username.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_PASSWORD, cfg.password.c_str());
|
||||||
|
}
|
||||||
|
curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_TRY);
|
||||||
|
// Allow self-signed certs on localhost relay — a common dev / pipe-transport setup.
|
||||||
|
if (cfg.host == "localhost" || cfg.host == "127.0.0.1") {
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep worker threads from blocking on a dead mail server indefinitely.
|
||||||
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
||||||
|
|
||||||
|
// Build MIME body: text/html body first, then attachments.
|
||||||
|
curl_mime* mime = curl_mime_init(curl);
|
||||||
|
|
||||||
|
curl_mimepart* bodyPart = curl_mime_addpart(mime);
|
||||||
|
curl_mime_data(bodyPart, htmlBody.c_str(), (curl_off_t)htmlBody.size());
|
||||||
|
curl_mime_type(bodyPart, "text/html; charset=utf-8");
|
||||||
|
curl_mime_encoder(bodyPart, "quoted-printable");
|
||||||
|
|
||||||
|
for (const auto& [fname, fcontent] : attachments) {
|
||||||
|
curl_mimepart* apart = curl_mime_addpart(mime);
|
||||||
|
curl_mime_data(apart, fcontent.c_str(), (curl_off_t)fcontent.size());
|
||||||
|
curl_mime_filename(apart, fname.c_str());
|
||||||
|
curl_mime_encoder(apart, "base64");
|
||||||
|
std::string mtype = "application/octet-stream";
|
||||||
|
if (fname.size() > 4) {
|
||||||
|
std::string ext = fname.substr(fname.size() - 4);
|
||||||
|
if (ext == ".pdf") mtype = "application/pdf";
|
||||||
|
else if (ext == ".ics") mtype = "text/calendar; charset=utf-8";
|
||||||
|
}
|
||||||
|
curl_mime_type(apart, mtype.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
|
||||||
|
|
||||||
|
// RFC 2047 encoded-word Subject so non-ASCII survives.
|
||||||
|
std::string encodedSubject = "=?UTF-8?B?" + base64Encode(subject) + "?=";
|
||||||
|
struct curl_slist* hdrs = nullptr;
|
||||||
|
hdrs = curl_slist_append(hdrs, ("From: " + cfg.fromAddress).c_str());
|
||||||
|
hdrs = curl_slist_append(hdrs, ("To: " + to).c_str());
|
||||||
|
hdrs = curl_slist_append(hdrs, ("Subject: " + encodedSubject).c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
std::string err;
|
||||||
|
if (res != CURLE_OK) err = std::string(curl_easy_strerror(res));
|
||||||
|
|
||||||
|
curl_mime_free(mime);
|
||||||
|
curl_slist_free_all(rcpt);
|
||||||
|
curl_slist_free_all(hdrs);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace oatpp_authkit::mail
|
||||||
|
|
||||||
|
#endif // oatpp_authkit_mail_SmtpTransport_hpp
|
||||||
Loading…
Add table
Reference in a new issue