LCOV - code coverage report
Current view: top level - libwrite/src - smtp_adapter.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 64.4 % 87 56
Test Date: 2026-04-15 21:12:52 Functions: 100.0 % 3 3

            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              : }
        

Generated by: LCOV version 2.0-1