Line data Source code
1 : #include "smtp_adapter.h"
2 : #include "logger.h"
3 : #include <curl/curl.h>
4 : #include <stdio.h>
5 : #include <stdlib.h>
6 : #include <string.h>
7 :
8 : /**
9 : * @file smtp_adapter.c
10 : * @brief libcurl-based SMTP adapter for sending RFC 2822 messages.
11 : */
12 :
13 : /* ── Read callback for libcurl upload ───────────────────────────────── */
14 :
15 : typedef struct {
16 : const char *data;
17 : size_t len;
18 : size_t pos;
19 : } ReadCtx;
20 :
21 2 : static size_t read_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
22 2 : ReadCtx *ctx = (ReadCtx *)userdata;
23 2 : size_t room = size * nmemb;
24 2 : size_t left = ctx->len - ctx->pos;
25 2 : if (left == 0) return 0;
26 1 : size_t n = left < room ? left : room;
27 1 : memcpy(ptr, ctx->data + ctx->pos, n);
28 1 : ctx->pos += n;
29 1 : return n;
30 : }
31 :
32 : /* ── URL construction ─────────────────────────────────────────────── */
33 :
34 : /**
35 : * Build the SMTP URL from config.
36 : * Returns heap-allocated string; caller must free().
37 : */
38 1 : static char *build_smtp_url(const Config *cfg) {
39 1 : char *url = NULL;
40 :
41 1 : if (cfg->smtp_host && cfg->smtp_host[0]) {
42 : /* Use configured SMTP host; append port if specified */
43 1 : if (cfg->smtp_port) {
44 : /* Check if port is already embedded in the URL */
45 0 : const char *after_scheme = strstr(cfg->smtp_host, "://");
46 0 : int has_port = after_scheme && strchr(after_scheme + 3, ':') != NULL;
47 0 : if (has_port) {
48 0 : url = strdup(cfg->smtp_host);
49 : } else {
50 0 : if (asprintf(&url, "%s:%d", cfg->smtp_host, cfg->smtp_port) < 0)
51 0 : url = NULL;
52 : }
53 : } else {
54 1 : url = strdup(cfg->smtp_host);
55 : }
56 0 : } else if (cfg->host) {
57 : /* Derive SMTP URL from IMAP URL */
58 0 : if (strncmp(cfg->host, "imaps://", 8) == 0) {
59 0 : if (asprintf(&url, "smtps://%s", cfg->host + 8) < 0) url = NULL;
60 0 : } else if (strncmp(cfg->host, "imap://", 7) == 0) {
61 0 : if (asprintf(&url, "smtp://%s", cfg->host + 7) < 0) url = NULL;
62 : }
63 : else
64 0 : url = strdup(cfg->host);
65 : } else {
66 0 : url = strdup("smtp://localhost");
67 : }
68 1 : return url;
69 : }
70 :
71 : /* ── Public API ──────────────────────────────────────────────────── */
72 :
73 1 : int smtp_send(const Config *cfg,
74 : const char *from,
75 : const char *to,
76 : const char *message,
77 : size_t message_len) {
78 1 : if (!cfg || !from || !to || !message) return -1;
79 :
80 1 : const char *smtp_user = cfg->smtp_user ? cfg->smtp_user : cfg->user;
81 1 : const char *smtp_pass = cfg->smtp_pass ? cfg->smtp_pass : cfg->pass;
82 :
83 1 : char *url = build_smtp_url(cfg);
84 1 : if (!url) {
85 0 : fprintf(stderr, "smtp_send: failed to build SMTP URL.\n");
86 0 : return -1;
87 : }
88 :
89 : /* Hard enforcement: only smtps:// (implicit TLS) is allowed.
90 : * Exception: cfg->ssl_no_verify=1 permits smtp:// for test environments. */
91 1 : if (strncmp(url, "smtps://", 8) != 0 && !cfg->ssl_no_verify) {
92 0 : fprintf(stderr,
93 : "smtp_send: refused to send via %s — "
94 : "only smtps:// is allowed (TLS required).\n"
95 : "Set SMTP_HOST=smtps://smtp.example.com in your config.\n"
96 : "For test environments only: add SSL_NO_VERIFY=1 to config.\n", url);
97 0 : logger_log(LOG_ERROR,
98 : "smtp_send: rejected insecure SMTP URL: %s", url);
99 0 : free(url);
100 0 : return -1;
101 : }
102 :
103 1 : CURL *curl = curl_easy_init();
104 1 : if (!curl) {
105 0 : fprintf(stderr, "smtp_send: curl_easy_init() failed.\n");
106 0 : free(url);
107 0 : return -1;
108 : }
109 :
110 : /* Envelope From: strip display name, extract bare address */
111 1 : char from_env[512];
112 1 : const char *lt = strchr(from, '<');
113 1 : const char *gt = lt ? strchr(lt, '>') : NULL;
114 1 : if (lt && gt && gt > lt) {
115 0 : size_t alen = (size_t)(gt - lt - 1);
116 0 : if (alen >= sizeof(from_env)) alen = sizeof(from_env) - 1;
117 0 : snprintf(from_env, sizeof(from_env), "<%.*s>", (int)alen, lt + 1);
118 : } else {
119 1 : snprintf(from_env, sizeof(from_env), "<%s>", from);
120 : }
121 :
122 1 : char to_env[512];
123 1 : const char *lt2 = strchr(to, '<');
124 1 : const char *gt2 = lt2 ? strchr(lt2, '>') : NULL;
125 1 : if (lt2 && gt2 && gt2 > lt2) {
126 0 : size_t alen = (size_t)(gt2 - lt2 - 1);
127 0 : if (alen >= sizeof(to_env)) alen = sizeof(to_env) - 1;
128 0 : snprintf(to_env, sizeof(to_env), "<%.*s>", (int)alen, lt2 + 1);
129 : } else {
130 1 : snprintf(to_env, sizeof(to_env), "<%s>", to);
131 : }
132 :
133 1 : struct curl_slist *rcpt = curl_slist_append(NULL, to_env);
134 :
135 1 : ReadCtx rctx = {message, message_len, 0};
136 :
137 1 : curl_easy_setopt(curl, CURLOPT_URL, url);
138 1 : curl_easy_setopt(curl, CURLOPT_USERNAME, smtp_user ? smtp_user : "");
139 1 : curl_easy_setopt(curl, CURLOPT_PASSWORD, smtp_pass ? smtp_pass : "");
140 1 : curl_easy_setopt(curl, CURLOPT_MAIL_FROM, from_env);
141 1 : curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, rcpt);
142 1 : curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);
143 1 : curl_easy_setopt(curl, CURLOPT_READDATA, &rctx);
144 1 : curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
145 :
146 : /* smtps:// = implicit TLS (port 465); enforce TLS on the connection */
147 1 : curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL);
148 :
149 : /* Honour ssl_no_verify for self-signed certs in test environments */
150 1 : if (cfg->ssl_no_verify) {
151 1 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
152 1 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
153 : }
154 :
155 1 : CURLcode res = curl_easy_perform(curl);
156 1 : int rc = 0;
157 1 : if (res != CURLE_OK) {
158 0 : fprintf(stderr, "smtp_send: %s\n", curl_easy_strerror(res));
159 0 : logger_log(LOG_ERROR, "smtp_send failed: %s", curl_easy_strerror(res));
160 0 : rc = -1;
161 : } else {
162 1 : logger_log(LOG_INFO, "smtp_send: message sent from %s to %s", from, to);
163 : }
164 :
165 1 : curl_slist_free_all(rcpt);
166 1 : curl_easy_cleanup(curl);
167 1 : free(url);
168 1 : return rc;
169 : }
|