verion 0.0.1 sources added;

This commit is contained in:
Alexander Vdolainen 2026-09-29 01:28:53 +03:00
parent edea3115bc
commit 007c81eebe
19 changed files with 2474 additions and 0 deletions

23
include/ilib/config.h Normal file
View File

@ -0,0 +1,23 @@
/* config.h - Subrelayd configuration loader */
#ifndef __CONFIG_H__
#define __CONFIG_H__
typedef struct subrelay_config {
char *maillist_addr; /* C001.1 */
char *imap_host; /* C001.2 */
int imap_port; /* C001.2 */
char *smtp_host; /* C001.3 */
int smtp_port; /* C001.3 */
char *account_pass; /* C001.4 */
char *maillist_name; /* C001.5 */
char *template_path; /* C001.6 */
char *run_user; /* C001.7 */
char *run_group; /* C001.7 */
char *socket_path; /* C001.8 */
char *subscribers_file;/* C001.9 */
} subrelay_config_t;
int config_load(subrelay_config_t *cfg, const char *filepath);
void config_free(subrelay_config_t *cfg);
#endif /* __CONFIG_H__ */

29
include/ilib/ipc.h Normal file
View File

@ -0,0 +1,29 @@
/* ipc.h - UNIX domain socket IPC listener and command processor */
#ifndef __IPC_H__
#define __IPC_H__
#include <ilib/config.h>
#include <ilib/subscribers.h>
#include <ilib/pending.h>
typedef struct ipc_server {
int server_fd;
char *socket_path;
subrelay_config_t *cfg;
subscriber_pool_t *subscribers;
pending_pool_t *pending;
} ipc_server_t;
/* Initialize non-blocking UNIX domain socket server */
int ipc_server_init(ipc_server_t *server,
subrelay_config_t *cfg,
subscriber_pool_t *subscribers,
pending_pool_t *pending);
/* Non-blocking check for incoming client commands from subrelay-shell */
int ipc_server_poll(ipc_server_t *server);
/* Cleanup socket file and close descriptors */
void ipc_server_destroy(ipc_server_t *server);
#endif /* __IPC_H__ */

View File

@ -0,0 +1,38 @@
/* mail_engine.h - IMAP fetching & SMTP fan-out engine using libcurl */
#ifndef __MAIL_ENGINE_H__
#define __MAIL_ENGINE_H__
#include <stddef.h>
#include <ilib/config.h>
typedef struct mail_message {
char *raw_data; /* Full raw RFC 822 byte payload */
size_t size; /* Size of raw payload */
unsigned long uid; /* IMAP Unique Identifier */
} mail_message_t;
typedef struct mail_list_results {
unsigned long *uids;
size_t count;
} mail_list_results_t;
/* Initialize and cleanup global libcurl environment */
int mail_engine_global_init(void);
void mail_engine_global_cleanup(void);
/* IMAP Client API */
int imap_fetch_unseen_uids(const subrelay_config_t *cfg, mail_list_results_t *results);
int imap_fetch_message(const subrelay_config_t *cfg, unsigned long uid, mail_message_t *msg);
int imap_expunge_message(const subrelay_config_t *cfg, unsigned long uid);
void mail_message_free(mail_message_t *msg);
void mail_list_results_free(mail_list_results_t *res);
/* SMTP Client API */
int smtp_send_message(const subrelay_config_t *cfg,
const char **recipients,
size_t recipient_count,
const char *raw_msg,
size_t msg_len);
#endif /* __MAIL_ENGINE_H__ */

View File

@ -0,0 +1,24 @@
/* mail_processor.h - Header parsing, header rewriting, and UC 4.2-4.5 routing */
#ifndef __MAIL_PROCESSOR_H__
#define __MAIL_PROCESSOR_H__
#include <ilib/config.h>
#include <ilib/subscribers.h>
#include <ilib/pending.h>
#include <ilib/mail_engine.h>
typedef struct mail_processor {
subrelay_config_t *cfg;
subscriber_pool_t *subscribers;
pending_pool_t *pending;
} mail_processor_t;
int mail_processor_init(mail_processor_t *mp,
subrelay_config_t *cfg,
subscriber_pool_t *subscribers,
pending_pool_t *pending);
/* Processes a single fetched message according to UC 4.2 - 4.5 */
int mail_processor_execute(mail_processor_t *mp, mail_message_t *msg);
#endif /* __MAIL_PROCESSOR_H__ */

47
include/ilib/pending.h Normal file
View File

@ -0,0 +1,47 @@
/* pending.h - Pending subscription state tracker using splay tree */
#ifndef __PENDING_H__
#define __PENDING_H__
#include <time.h>
#include <ilib/splay.h>
#define PENDING_ID_LEN 16
typedef struct pending_entry {
splay_node_t node;
char token[PENDING_ID_LEN + 1]; /* Unique hex string token */
char *email; /* Applicant email address */
time_t created_at; /* Unix timestamp for TTL check */
} pending_entry_t;
typedef struct pending_pool {
splay_t tree;
char *filepath;
long ttl_seconds; /* Default TTL: e.g., 86400 (24h) */
} pending_pool_t;
int pending_pool_init(pending_pool_t *pool, const char *filepath, long ttl_seconds);
void pending_pool_destroy(pending_pool_t *pool);
/*
* Generates a unique token, attaches the email address, and inserts into tree.
* Populates out_token with the generated hex identifier string.
*/
int pending_add(pending_pool_t *pool, const char *email, char *out_token, size_t token_buf_len);
/*
* Looks up a pending token. If found and not expired, returns the associated applicant email.
* If expired, automatically purges the entry and returns NULL.
*/
char *pending_lookup_email(pending_pool_t *pool, const char *token);
/* Explicitly removes a pending token after successful confirmation */
int pending_remove(pending_pool_t *pool, const char *token);
/* Purges stale pending entries exceeding the TTL */
void pending_purge_expired(pending_pool_t *pool);
int pending_load_file(pending_pool_t *pool);
int pending_save_file(pending_pool_t *pool);
#endif /* __PENDING_H__ */

36
include/ilib/splay.h Normal file
View File

@ -0,0 +1,36 @@
/* splay tree implementation */
#ifndef __SPLAY_H__
#define __SPLAY_H__
#include <stdint.h>
#define tree_root_priv(T) ((T)->splay_sentinel.splay_left)
#define tree_null_priv(L) (&(L)->splay_sentinel)
#define TREE_DEPTH_MAX 64
typedef long (*splay_compare_t)(const void*, const void*);
typedef struct __splay_node_t {
struct __splay_node_t *splay_left;
struct __splay_node_t *splay_right;
struct __splay_node_t *splay_parent;
void *splay_data;
const void *splay_node_key;
} splay_node_t;
typedef struct __splay_t {
uint64_t nodecount;
uint64_t maxcount;
splay_node_t splay_sentinel;
splay_compare_t splay_compare;
void *splay_context;
} splay_t;
int splay_init(splay_t *sp, splay_compare_t cmp_fu);
int splay_insert(splay_t *sp, splay_node_t *node, const void *node_key);
int splay_delete(splay_t *sp, splay_node_t *node);
splay_node_t *splay_lookup(splay_t *sp, const void *key);
splay_node_t *splay_first(splay_t *sp);
splay_node_t *splay_next(splay_t *sp, splay_node_t *curr);
#endif /* __SPLAY_H__*/

View File

@ -0,0 +1,28 @@
/* subscribers.h - Subscriber pool & persistence management */
#ifndef __SUBSCRIBERS_H__
#define __SUBSCRIBERS_H__
#include <ilib/splay.h>
typedef struct subscriber_entry {
splay_node_t node;
char *email;
} subscriber_entry_t;
/* Global state wrapper */
typedef struct subscriber_pool {
splay_t tree;
char *filepath;
} subscriber_pool_t;
int subscriber_pool_init(subscriber_pool_t *pool, const char *filepath);
void subscriber_pool_destroy(subscriber_pool_t *pool);
int subscriber_add(subscriber_pool_t *pool, const char *email);
int subscriber_remove(subscriber_pool_t *pool, const char *email);
int subscriber_exists(subscriber_pool_t *pool, const char *email);
int subscriber_load_file(subscriber_pool_t *pool);
int subscriber_save_file(subscriber_pool_t *pool);
#endif /* __SUBSCRIBERS_H__ */

27
include/ilib/template.h Normal file
View File

@ -0,0 +1,27 @@
/* template.h - Template parser and string variable substitution engine */
#ifndef __TEMPLATE_H__
#define __TEMPLATE_H__
#include <stddef.h>
typedef struct template_var {
const char *key; /* e.g., "MAILLIST_NAME" (without the '$') */
const char *val; /* e.g., "Civilisation List" */
} template_var_t;
/*
* Renders a template file from disk, substituting all $KEY matches.
*
* @param template_path Path to the template text file
* @param vars Array of key-value replacement pairs
* @param var_count Number of entries in vars array
* @return Dynamically allocated string (caller must free()), or NULL on error.
*/
char *template_render_file(const char *template_path, const template_var_t *vars, size_t var_count);
/*
* Renders a template string from memory, substituting all $KEY matches.
*/
char *template_render_string(const char *template_src, const template_var_t *vars, size_t var_count);
#endif /* __TEMPLATE_H__ */

39
src/Makefile.am Normal file
View File

@ -0,0 +1,39 @@
bin_PROGRAMS = subrelayd subrelay-shell
noinst_HEADERS = \
$(top_srcdir)/include/ilib/config.h \
$(top_srcdir)/include/ilib/ipc.h \
$(top_srcdir)/include/ilib/mail_engine.h \
$(top_srcdir)/include/ilib/mail_processor.h \
$(top_srcdir)/include/ilib/pending.h \
$(top_srcdir)/include/ilib/splay.h \
$(top_srcdir)/include/ilib/subscribers.h \
$(top_srcdir)/include/ilib/template.h
# ------------------------------------------------------------------------------
# subrelayd Daemon
# ------------------------------------------------------------------------------
subrelayd_SOURCES = \
subrelayd.c \
splay.c \
subscribers.c \
pending.c \
config.c \
template.c \
mail_engine.c \
mail_processor.c \
ipc.c
# Include internal header search path and PKG_CHECK libcurl flags
subrelayd_CPPFLAGS = -I$(top_srcdir)/include -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE $(CURL_CFLAGS)
subrelayd_CFLAGS = -Wall -Wextra -pedantic -std=c99
subrelayd_LDADD = $(CURL_LIBS)
# ------------------------------------------------------------------------------
# subrelay-shell CLI Tool
# ------------------------------------------------------------------------------
subrelay_shell_SOURCES = \
subrelay-shell.c
subrelay_shell_CFLAGS = -Wall -Wextra -pedantic -std=c99
subrelay_shell_CPPFLAGS = -I$(top_srcdir)/include -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE

138
src/config.c Normal file
View File

@ -0,0 +1,138 @@
/* config.c - Key-value configuration parser for subrelayd */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <ilib/config.h>
static char *trim(char *str)
{
while (isspace((unsigned char)*str)) str++;
if (*str == 0) return str;
char *end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end)) end--;
end[1] = '\0';
return str;
}
/* Strips quotes if value is wrapped in 'VALUE' or "VALUE" */
static char *strip_quotes(char *val)
{
val = trim(val);
size_t len = strlen(val);
if (len >= 2 && ((val[0] == '\'' && val[len - 1] == '\'') ||
(val[0] == '"' && val[len - 1] == '"'))) {
val[len - 1] = '\0';
val++;
}
return val;
}
static void set_string_param(char **target, const char *val)
{
if (*target) free(*target);
*target = strdup(val);
}
int config_load(subrelay_config_t *cfg, const char *filepath)
{
if (!cfg || !filepath) return -EINVAL;
memset(cfg, 0, sizeof(subrelay_config_t));
/* Apply sensible defaults */
cfg->imap_port = 993;
cfg->smtp_port = 587;
cfg->socket_path = strdup("/var/run/subrelayd.sock");
cfg->subscribers_file = strdup("/etc/subrelay/subscribers.txt");
cfg->run_user = strdup("subrelay");
cfg->run_group = strdup("subrelay");
FILE *fp = fopen(filepath, "r");
if (!fp) return -errno;
char line[1024];
int line_num = 0;
while (fgets(line, sizeof(line), fp)) {
line_num++;
/* Rule C004.1: Everything after '#' is ignored */
char *comment = strchr(line, '#');
if (comment) *comment = '\0';
char *trimmed = trim(line);
if (*trimmed == '\0') continue; /* Skip empty/comment lines */
/* Rule C004.2: <KEY>: '<VALUE>' */
char *colon = strchr(trimmed, ':');
if (!colon) {
fprintf(stderr, "config: syntax error at line %d: missing ':'\n", line_num);
continue;
}
*colon = '\0';
char *key = trim(trimmed);
char *value = strip_quotes(colon + 1);
if (strcasecmp(key, "MAILLIST_ADDR") == 0) {
set_string_param(&cfg->maillist_addr, value);
} else if (strcasecmp(key, "IMAP_HOST") == 0) {
set_string_param(&cfg->imap_host, value);
} else if (strcasecmp(key, "IMAP_PORT") == 0) {
cfg->imap_port = atoi(value);
} else if (strcasecmp(key, "SMTP_HOST") == 0) {
set_string_param(&cfg->smtp_host, value);
} else if (strcasecmp(key, "SMTP_PORT") == 0) {
cfg->smtp_port = atoi(value);
} else if (strcasecmp(key, "ACCOUNT_PASS") == 0) {
set_string_param(&cfg->account_pass, value);
} else if (strcasecmp(key, "MAILLIST_NAME") == 0) {
set_string_param(&cfg->maillist_name, value);
} else if (strcasecmp(key, "TEMPLATE_PATH") == 0) {
set_string_param(&cfg->template_path, value);
} else if (strcasecmp(key, "RUN_USER") == 0) {
set_string_param(&cfg->run_user, value);
} else if (strcasecmp(key, "RUN_GROUP") == 0) {
set_string_param(&cfg->run_group, value);
} else if (strcasecmp(key, "SOCKET_PATH") == 0) {
set_string_param(&cfg->socket_path, value);
} else if (strcasecmp(key, "SUBSCRIBERS_FILE") == 0) {
set_string_param(&cfg->subscribers_file, value);
}
}
fclose(fp);
/* Validate essential parameters */
if (!cfg->maillist_addr || !cfg->imap_host || !cfg->smtp_host || !cfg->account_pass) {
fprintf(stderr, "config error: missing mandatory credentials (MAILLIST_ADDR, IMAP_HOST, SMTP_HOST, ACCOUNT_PASS)\n");
config_free(cfg);
return -EINVAL;
}
return 0;
}
void config_free(subrelay_config_t *cfg)
{
if (!cfg) return;
if (cfg->maillist_addr) free(cfg->maillist_addr);
if (cfg->imap_host) free(cfg->imap_host);
if (cfg->smtp_host) free(cfg->smtp_host);
if (cfg->account_pass) free(cfg->account_pass);
if (cfg->maillist_name) free(cfg->maillist_name);
if (cfg->template_path) free(cfg->template_path);
if (cfg->run_user) free(cfg->run_user);
if (cfg->run_group) free(cfg->run_group);
if (cfg->socket_path) free(cfg->socket_path);
if (cfg->subscribers_file)free(cfg->subscribers_file);
memset(cfg, 0, sizeof(subrelay_config_t));
}

195
src/ipc.c Normal file
View File

@ -0,0 +1,195 @@
/* ipc.c - UNIX domain socket ASCII command listener */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <ilib/ipc.h>
static char *trim(char *str)
{
while (isspace((unsigned char)*str)) str++;
if (*str == 0) return str;
char *end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end)) end--;
end[1] = '\0';
return str;
}
/* Parse and dispatch ASCII commands from subrelay-shell */
static void handle_ipc_command(ipc_server_t *server, int client_fd, char *cmd_line)
{
char *line = trim(cmd_line);
if (*line == '\0') return;
char *cmd = strtok(line, " \t");
char *arg = strtok(NULL, " \t");
char response[2048];
response[0] = '\0';
if (!cmd) {
snprintf(response, sizeof(response), "ERR invalid_command\n");
}
/* ---------------------------------------------------------------------- */
/* Command: ADD <email> (Directly subscribe address, bypasses opt-in) */
/* ---------------------------------------------------------------------- */
else if (strcasecmp(cmd, "ADD") == 0) {
if (!arg) {
snprintf(response, sizeof(response), "ERR missing_email_argument\n");
} else {
int rc = subscriber_add(server->subscribers, arg);
if (rc == 0) {
subscriber_save_file(server->subscribers);
snprintf(response, sizeof(response), "OK subscriber_added %s\n", arg);
} else if (rc == -EEXIST) {
snprintf(response, sizeof(response), "ERR subscriber_already_exists\n");
} else {
snprintf(response, sizeof(response), "ERR failed_to_add_subscriber\n");
}
}
}
/* ---------------------------------------------------------------------- */
/* Command: DEL <email> (Directly unsubscribe address) */
/* ---------------------------------------------------------------------- */
else if (strcasecmp(cmd, "DEL") == 0) {
if (!arg) {
snprintf(response, sizeof(response), "ERR missing_email_argument\n");
} else {
int rc = subscriber_remove(server->subscribers, arg);
if (rc == 0) {
subscriber_save_file(server->subscribers);
snprintf(response, sizeof(response), "OK subscriber_removed %s\n", arg);
} else {
snprintf(response, sizeof(response), "ERR subscriber_not_found\n");
}
}
}
/* ---------------------------------------------------------------------- */
/* Command: LIST (Output current subscribers) */
/* ---------------------------------------------------------------------- */
else if (strcasecmp(cmd, "LIST") == 0) {
snprintf(response, sizeof(response), "OK SUBSCRIBERS %lu\n",
(unsigned long)server->subscribers->tree.nodecount);
write(client_fd, response, strlen(response));
response[0] = '\0';
splay_node_t *curr = splay_first(&server->subscribers->tree);
while (curr != NULL) {
subscriber_entry_t *entry = (subscriber_entry_t *)curr;
char item[512];
snprintf(item, sizeof(item), "%s\n", entry->email);
write(client_fd, item, strlen(item));
curr = splay_next(&server->subscribers->tree, curr);
}
snprintf(response, sizeof(response), "END\n");
}
/* ---------------------------------------------------------------------- */
/* Command: STATUS (Output system stats & daemon info) */
/* ---------------------------------------------------------------------- */
else if (strcasecmp(cmd, "STATUS") == 0) {
snprintf(response, sizeof(response),
"OK LIST_NAME='%s' ADDRESS='%s' SUBSCRIBERS=%lu PENDING=%lu\n",
server->cfg->maillist_name,
server->cfg->maillist_addr,
(unsigned long)server->subscribers->tree.nodecount,
(unsigned long)server->pending->tree.nodecount);
}
else {
snprintf(response, sizeof(response), "ERR unknown_command\n");
}
if (strlen(response) > 0) {
write(client_fd, response, strlen(response));
}
}
int ipc_server_init(ipc_server_t *server, subrelay_config_t *cfg, subscriber_pool_t *subscribers, pending_pool_t *pending)
{
if (!server || !cfg || !cfg->socket_path) return -EINVAL;
memset(server, 0, sizeof(ipc_server_t));
server->cfg = cfg;
server->subscribers = subscribers;
server->pending = pending;
server->socket_path = strdup(cfg->socket_path);
/* Unlink old socket file if left over from previous unclean shutdown */
unlink(server->socket_path);
server->server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (server->server_fd < 0) return -errno;
/* Set socket non-blocking so main loop does not block on accept() */
int flags = fcntl(server->server_fd, F_GETFL, 0);
fcntl(server->server_fd, F_SETFL, flags | O_NONBLOCK);
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, server->socket_path, sizeof(addr.sun_path) - 1);
if (bind(server->server_fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
close(server->server_fd);
return -errno;
}
/* Restrict socket file permissions: rw-rw---- (owner & group access only) */
chmod(server->socket_path, 0660);
if (listen(server->server_fd, 5) != 0) {
close(server->server_fd);
unlink(server->socket_path);
return -errno;
}
return 0;
}
int ipc_server_poll(ipc_server_t *server)
{
if (!server || server->server_fd < 0) return -EINVAL;
int client_fd = accept(server->server_fd, NULL, NULL);
if (client_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return 0; /* No pending connection */
}
return -errno;
}
char buf[1024];
ssize_t bytes = read(client_fd, buf, sizeof(buf) - 1);
if (bytes > 0) {
buf[bytes] = '\0';
handle_ipc_command(server, client_fd, buf);
}
close(client_fd);
return 0;
}
void ipc_server_destroy(ipc_server_t *server)
{
if (!server) return;
if (server->server_fd >= 0) {
close(server->server_fd);
}
if (server->socket_path) {
unlink(server->socket_path);
free(server->socket_path);
}
memset(server, 0, sizeof(ipc_server_t));
}

239
src/mail_engine.c Normal file
View File

@ -0,0 +1,239 @@
/* mail_engine.c - libcurl IMAP and SMTP implementation */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <curl/curl.h>
#include <ilib/mail_engine.h>
typedef struct memory_buffer {
char *data;
size_t size;
} memory_buffer_t;
typedef struct upload_buffer {
const char *data;
size_t bytes_remaining;
} upload_buffer_t;
/* Callback for libcurl to append fetched HTTP/IMAP bytes into memory */
static size_t write_memory_callback(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t real_size = size * nmemb;
memory_buffer_t *mem = (memory_buffer_t *)userp;
char *ptr = realloc(mem->data, mem->size + real_size + 1);
if (!ptr) return 0; /* Out of memory */
mem->data = ptr;
memcpy(&(mem->data[mem->size]), contents, real_size);
mem->size += real_size;
mem->data[mem->size] = 0;
return real_size;
}
/* Callback for libcurl to read bytes when sending outbound mail via SMTP */
static size_t read_payload_callback(char *buffer, size_t size, size_t nitems, void *userp)
{
upload_buffer_t *upload = (upload_buffer_t *)userp;
size_t max_bytes = size * nitems;
if (upload->bytes_remaining == 0) return 0; /* EOF */
size_t bytes_to_copy = (upload->bytes_remaining < max_bytes) ? upload->bytes_remaining : max_bytes;
memcpy(buffer, upload->data, bytes_to_copy);
upload->data += bytes_to_copy;
upload->bytes_remaining -= bytes_to_copy;
return bytes_to_copy;
}
int mail_engine_global_init(void)
{
return (curl_global_init(CURL_GLOBAL_ALL) == CURLE_OK) ? 0 : -1;
}
void mail_engine_global_cleanup(void)
{
curl_global_cleanup();
}
/* -------------------------------------------------------------------------- */
/* IMAP Operations */
/* -------------------------------------------------------------------------- */
int imap_fetch_unseen_uids(const subrelay_config_t *cfg, mail_list_results_t *results)
{
if (!cfg || !results) return -EINVAL;
memset(results, 0, sizeof(mail_list_results_t));
CURL *curl = curl_easy_init();
if (!curl) return -ENOMEM;
char url[512];
snprintf(url, sizeof(url), "imaps://%s:%d/INBOX", cfg->imap_host, cfg->imap_port);
memory_buffer_t buf = { calloc(1, 1), 0 };
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_USERNAME, cfg->maillist_addr);
curl_easy_setopt(curl, CURLOPT_PASSWORD, cfg->account_pass);
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "UID SEARCH UNSEEN");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_memory_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&buf);
curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL);
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
free(buf.data);
return -EIO;
}
/* Parse IMAP response string (e.g., "* SEARCH 101 102 103\r\n") */
char *search_str = strstr(buf.data, "* SEARCH");
if (search_str) {
search_str += 8; /* Skip "* SEARCH" */
char *token = strtok(search_str, " \r\n");
while (token) {
unsigned long uid = strtoul(token, NULL, 10);
if (uid > 0) {
results->uids = realloc(results->uids, sizeof(unsigned long) * (results->count + 1));
results->uids[results->count++] = uid;
}
token = strtok(NULL, " \r\n");
}
}
free(buf.data);
return 0;
}
int imap_fetch_message(const subrelay_config_t *cfg, unsigned long uid, mail_message_t *msg)
{
if (!cfg || !msg || uid == 0) return -EINVAL;
memset(msg, 0, sizeof(mail_message_t));
CURL *curl = curl_easy_init();
if (!curl) return -ENOMEM;
char url[512];
snprintf(url, sizeof(url), "imaps://%s:%d/INBOX;UID=%lu", cfg->imap_host, cfg->imap_port, uid);
memory_buffer_t buf = { calloc(1, 1), 0 };
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_USERNAME, cfg->maillist_addr);
curl_easy_setopt(curl, CURLOPT_PASSWORD, cfg->account_pass);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_memory_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&buf);
curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL);
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
free(buf.data);
return -EIO;
}
msg->raw_data = buf.data;
msg->size = buf.size;
msg->uid = uid;
return 0;
}
int imap_expunge_message(const subrelay_config_t *cfg, unsigned long uid)
{
if (!cfg || uid == 0) return -EINVAL;
CURL *curl = curl_easy_init();
if (!curl) return -ENOMEM;
char url[512];
snprintf(url, sizeof(url), "imaps://%s:%d/INBOX", cfg->imap_host, cfg->imap_port);
char custom_req[128];
snprintf(custom_req, sizeof(custom_req), "UID STORE %lu +FLAGS (\\Deleted)", uid);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_USERNAME, cfg->maillist_addr);
curl_easy_setopt(curl, CURLOPT_PASSWORD, cfg->account_pass);
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, custom_req);
curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL);
CURLcode res = curl_easy_perform(curl);
if (res == CURLE_OK) {
/* Issue EXPUNGE to clear deleted items */
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "EXPUNGE");
curl_easy_perform(curl);
}
curl_easy_cleanup(curl);
return (res == CURLE_OK) ? 0 : -EIO;
}
/* -------------------------------------------------------------------------- */
/* SMTP Operations */
/* -------------------------------------------------------------------------- */
int smtp_send_message(const subrelay_config_t *cfg,
const char **recipients,
size_t recipient_count,
const char *raw_msg,
size_t msg_len)
{
if (!cfg || !recipients || recipient_count == 0 || !raw_msg) return -EINVAL;
CURL *curl = curl_easy_init();
if (!curl) return -ENOMEM;
char url[512];
snprintf(url, sizeof(url), "smtps://%s:%d", cfg->smtp_host, cfg->smtp_port);
struct curl_slist *recip_list = NULL;
for (size_t i = 0; i < recipient_count; i++) {
recip_list = curl_slist_append(recip_list, recipients[i]);
}
upload_buffer_t upload = { raw_msg, msg_len };
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_USERNAME, cfg->maillist_addr);
curl_easy_setopt(curl, CURLOPT_PASSWORD, cfg->account_pass);
curl_easy_setopt(curl, CURLOPT_MAIL_FROM, cfg->maillist_addr);
curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recip_list);
curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_payload_callback);
curl_easy_setopt(curl, CURLOPT_READDATA, &upload);
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(recip_list);
curl_easy_cleanup(curl);
return (res == CURLE_OK) ? 0 : -EIO;
}
void mail_message_free(mail_message_t *msg)
{
if (msg) {
if (msg->raw_data) free(msg->raw_data);
memset(msg, 0, sizeof(mail_message_t));
}
}
void mail_list_results_free(mail_list_results_t *res)
{
if (res) {
if (res->uids) free(res->uids);
memset(res, 0, sizeof(mail_list_results_t));
}
}

466
src/mail_processor.c Normal file
View File

@ -0,0 +1,466 @@
/* mail_processor.c - RFC 822 header parsing & routing engine */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <errno.h>
#include <ilib/mail_processor.h>
#include <ilib/template.h>
/* Trims whitespace and trailing \r\n */
static char *trim(char *str)
{
while (isspace((unsigned char)*str)) str++;
if (*str == 0) return str;
char *end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end)) end--;
end[1] = '\0';
return str;
}
/* Extract raw address from formats like "Alice <alice@domain.tld>" */
static void extract_email_address(const char *raw, char *out, size_t out_len)
{
const char *start = strchr(raw, '<');
const char *end = strchr(raw, '>');
if (start && end && end > start) {
size_t len = end - start - 1;
if (len >= out_len) len = out_len - 1;
strncpy(out, start + 1, len);
out[len] = '\0';
} else {
snprintf(out, out_len, "%s", raw);
}
trim(out);
}
/* Case-insensitive header value extractor supporting multi-line folding */
static char *get_header_value(const char *raw_msg, const char *header_name)
{
size_t hlen = strlen(header_name);
const char *p = raw_msg;
while (*p) {
/* End of headers block reached (double CRLF/LF) */
if (p[0] == '\r' && p[1] == '\n' && (p[2] == '\r' || p[2] == '\n')) break;
if (p[0] == '\n' && p[1] == '\n') break;
if (strncasecmp(p, header_name, hlen) == 0 && p[hlen] == ':') {
p += hlen + 1;
while (*p == ' ' || *p == '\t') p++;
const char *val_start = p;
while (*p && *p != '\r' && *p != '\n') p++;
size_t val_len = p - val_start;
char *val = malloc(val_len + 1);
if (!val) return NULL;
memcpy(val, val_start, val_len);
val[val_len] = '\0';
return trim(val);
}
/* Move to next line */
while (*p && *p != '\n') p++;
if (*p == '\n') p++;
}
return NULL;
}
/* Checks if a header key should be stripped (E002 & E004) */
static int should_strip_header(const char *line)
{
/* Advance past any leading CRLF/whitespace */
while (*line == '\r' || *line == '\n' || *line == ' ' || *line == '\t') line++;
static const char *strip_list[] = {
"From:",
"Resent-From:",
"X-Original-From:",
"Envelope-From:",
"Reply-To:",
"Sender:",
"Precedence:",
"Auto-Submitted:",
"List-Id:",
"List-Unsubscribe:",
"List-Post:",
"List-Help:",
"List-Subscribe:",
"List-Archive:",
"DKIM-Signature:",
"DomainKey-Signature:",
"Authentication-Results:",
"Received:",
"Received-SPF:",
"Delivered-To:",
"Return-Path:",
"X-Received:",
"X-Spam-",
"X-Google-",
"X-Zoho-",
"X-Gm-",
"ARC-",
NULL
};
for (int i = 0; strip_list[i] != NULL; i++) {
if (strncasecmp(line, strip_list[i], strlen(strip_list[i])) == 0)
return 1;
}
return 0;
}
static int is_header_continuation(const char *line)
{
return (line[0] == ' ' || line[0] == '\t');
}
/* Rebuilds raw email buffer: strips stale/auth headers, injects E001 headers */
static char *rebuild_message_for_broadcast(mail_processor_t *mp, const char *raw_msg, size_t *out_len)
{
const char *body_start = strstr(raw_msg, "\r\n\r\n");
if (!body_start) body_start = strstr(raw_msg, "\n\n");
if (body_start) {
body_start += (body_start[0] == '\r') ? 4 : 2;
} else {
body_start = raw_msg + strlen(raw_msg);
}
/* Extract display name or email from original From: header */
char orig_display[128] = "Subscriber";
char *raw_from = get_header_value(raw_msg, "From");
if (raw_from) {
char *bracket = strchr(raw_from, '<');
if (bracket && bracket > raw_from) {
size_t dlen = bracket - raw_from;
if (dlen >= sizeof(orig_display)) dlen = sizeof(orig_display) - 1;
strncpy(orig_display, raw_from, dlen);
orig_display[dlen] = '\0';
trim(orig_display);
if (orig_display[0] == '"' || orig_display[0] == '\'') {
size_t slen = strlen(orig_display);
if (slen > 1 && (orig_display[slen - 1] == '"' || orig_display[slen - 1] == '\'')) {
orig_display[slen - 1] = '\0';
memmove(orig_display, orig_display + 1, slen - 1);
}
}
} else {
extract_email_address(raw_from, orig_display, sizeof(orig_display));
}
free(raw_from);
}
/* Extract local and domain parts */
char local_part[128] = {0};
char domain_part[128] = {0};
const char *at_sign = strchr(mp->cfg->maillist_addr, '@');
if (at_sign) {
size_t len = at_sign - mp->cfg->maillist_addr;
strncpy(local_part, mp->cfg->maillist_addr, len);
strcpy(domain_part, at_sign + 1);
} else {
strcpy(local_part, "subrelay");
strcpy(domain_part, "localhost");
}
size_t buf_cap = strlen(raw_msg) + 4096;
char *out = malloc(buf_cap);
if (!out) return NULL;
size_t pos = 0;
/* 1. Inject EXACTLY ONE From header line at the absolute top */
pos += snprintf(out + pos, buf_cap - pos,
"From: \"%s via %s\" <%s>\r\n"
"Reply-To: <%s>\r\n"
"Sender: <%s>\r\n"
"Precedence: list\r\n"
"Auto-Submitted: auto-generated\r\n"
"List-Id: <%s>\r\n"
"List-Unsubscribe: <mailto:%s+unsubscribe@%s>\r\n"
"List-Post: <mailto:%s>\r\n",
orig_display, mp->cfg->maillist_name, mp->cfg->maillist_addr,
mp->cfg->maillist_addr,
mp->cfg->maillist_addr,
mp->cfg->maillist_addr,
local_part, domain_part,
mp->cfg->maillist_addr
);
const char *p = raw_msg;
int stripping_current_header = 0;
int has_subject = 0;
/* 2. Line-by-line header processing */
while (p < body_start) {
const char *line_end = strchr(p, '\n');
if (!line_end || line_end >= body_start) break;
size_t line_len = line_end - p + 1;
/* Skip leading \r if present at start of line */
const char *check_ptr = p;
if (*check_ptr == '\r') check_ptr++;
if (is_header_continuation(check_ptr)) {
if (stripping_current_header) {
p = line_end + 1;
continue;
}
} else {
if (should_strip_header(check_ptr)) {
stripping_current_header = 1;
p = line_end + 1;
continue;
}
stripping_current_header = 0;
}
/* Subject Tagging (E003) */
if (strncasecmp(check_ptr, "Subject:", 8) == 0) {
has_subject = 1;
char tag[256];
snprintf(tag, sizeof(tag), "[%s]", mp->cfg->maillist_name);
if (!strstr(check_ptr, tag)) {
pos += snprintf(out + pos, buf_cap - pos, "Subject: %s ", tag);
/* Advance check_ptr past "Subject:" */
const char *subj_val = check_ptr + 8;
size_t val_len = (line_end - subj_val + 1);
memcpy(out + pos, subj_val, val_len);
pos += val_len;
out[pos] = '\0';
p = line_end + 1;
continue;
}
}
/* Copy clean header */
if (pos + line_len < buf_cap) {
memcpy(out + pos, p, line_len);
pos += line_len;
out[pos] = '\0';
}
p = line_end + 1;
}
if (!has_subject) {
pos += snprintf(out + pos, buf_cap - pos,
"Subject: [%s] (No Subject)\r\n", mp->cfg->maillist_name);
}
/* 3. Terminate Header Block with CRLF and append Body */
pos += snprintf(out + pos, buf_cap - pos, "\r\n");
size_t body_len = strlen(body_start);
if (pos + body_len < buf_cap) {
memcpy(out + pos, body_start, body_len);
pos += body_len;
out[pos] = '\0';
}
*out_len = pos;
return out;
}
int mail_processor_init(mail_processor_t *mp, subrelay_config_t *cfg, subscriber_pool_t *subscribers, pending_pool_t *pending)
{
if (!mp || !cfg || !subscribers || !pending) return -EINVAL;
mp->cfg = cfg;
mp->subscribers = subscribers;
mp->pending = pending;
return 0;
}
int mail_processor_execute(mail_processor_t *mp, mail_message_t *msg)
{
if (!mp || !msg || !msg->raw_data) return -EINVAL;
char *raw_from = get_header_value(msg->raw_data, "From");
char *raw_to = get_header_value(msg->raw_data, "To");
if (!raw_from || !raw_to) {
if (raw_from) free(raw_from);
if (raw_to) free(raw_to);
/* Corrupt or unparseable headers -> drop and expunge */
return imap_expunge_message(mp->cfg, msg->uid);
}
char sender[256], recipient[256];
extract_email_address(raw_from, sender, sizeof(sender));
extract_email_address(raw_to, recipient, sizeof(recipient));
free(raw_from);
free(raw_to);
/* ---------------------------------------------------------------------- */
/* UC 4.3: Subscription Initiation (maillist+subscribe@domain.tld) */
/* ---------------------------------------------------------------------- */
if (strcasecmp(recipient, "subscribe") == 0 || strstr(recipient, "+subscribe@")) {
if (!subscriber_exists(mp->subscribers, sender)) {
char token[32];
if (pending_add(mp->pending, sender, token, sizeof(token)) == 0) {
pending_save_file(mp->pending);
char confirm_addr[512];
char local_part[128] = {0};
char domain_part[128] = {0};
const char *at_sign = strchr(mp->cfg->maillist_addr, '@');
if (at_sign) {
size_t local_len = at_sign - mp->cfg->maillist_addr;
strncpy(local_part, mp->cfg->maillist_addr, local_len);
strcpy(domain_part, at_sign + 1);
} else {
strcpy(local_part, "subrelay");
strcpy(domain_part, "localhost");
}
snprintf(confirm_addr, sizeof(confirm_addr), "%s+%s@%s",
local_part, token, domain_part);
template_var_t vars[] = {
{ "MAILLIST_NAME", mp->cfg->maillist_name },
{ "MAILLIST_ADDR", mp->cfg->maillist_addr },
{ "CONFIRM_ADDR", confirm_addr },
{ "SUBSCRIBER_ADDR", sender }
};
char tpath[1024];
snprintf(tpath, sizeof(tpath), "%s/subscribe.txt", mp->cfg->template_path);
char *body = template_render_file(tpath, vars, 4);
if (body) {
const char *recips[] = { sender };
time_t now = time(NULL);
char date_buf[128];
struct tm *gmt = gmtime(&now);
strftime(date_buf, sizeof(date_buf), "%a, %d %b %Y %H:%M:%S +0000", gmt);
size_t full_msg_len = strlen(body) + 1024;
char *full_msg = malloc(full_msg_len);
snprintf(full_msg, full_msg_len,
"From: \"%s\" <%s>\r\n"
"To: <%s>\r\n"
"Reply-To: <%s>\r\n"
"Subject: Confirm your subscription to %s\r\n"
"Date: %s\r\n"
"Content-Type: text/plain; charset=utf-8\r\n"
"\r\n"
"%s",
mp->cfg->maillist_name, mp->cfg->maillist_addr,
sender,
confirm_addr,
mp->cfg->maillist_name,
date_buf,
body);
smtp_send_message(mp->cfg, recips, 1, full_msg, strlen(full_msg));
free(body);
free(full_msg);
}
}
}
return imap_expunge_message(mp->cfg, msg->uid);
}
/* ---------------------------------------------------------------------- */
/* UC 4.5: Unsubscribe (maillist+unsubscribe@domain.tld) */
/* ---------------------------------------------------------------------- */
if (strcasecmp(recipient, "unsubscribe") == 0 || strstr(recipient, "+unsubscribe@")) {
if (subscriber_exists(mp->subscribers, sender)) {
subscriber_remove(mp->subscribers, sender);
subscriber_save_file(mp->subscribers);
}
return imap_expunge_message(mp->cfg, msg->uid);
}
/* ---------------------------------------------------------------------- */
/* UC 4.4: Subscription Confirmation (maillist+<unique_id>@domain.tld) */
/* ---------------------------------------------------------------------- */
char *plus = strchr(recipient, '+');
char *at = strchr(recipient, '@');
if (plus && at && at > plus) {
size_t token_len = at - plus - 1;
char token[64];
if (token_len < sizeof(token)) {
strncpy(token, plus + 1, token_len);
token[token_len] = '\0';
char *pending_email = pending_lookup_email(mp->pending, token);
if (pending_email && strcasecmp(pending_email, sender) == 0) {
/* Add member to list */
subscriber_add(mp->subscribers, sender);
subscriber_save_file(mp->subscribers);
pending_remove(mp->pending, token);
pending_save_file(mp->pending);
/* Send Welcome Message */
template_var_t vars[] = {
{ "MAILLIST_NAME", mp->cfg->maillist_name },
{ "MAILLIST_ADDR", mp->cfg->maillist_addr },
{ "SUBSCRIBER_ADDR", sender }
};
char tpath[1024];
snprintf(tpath, sizeof(tpath), "%s/welcome.txt", mp->cfg->template_path);
char *body = template_render_file(tpath, vars, 3);
if (body) {
const char *recips[] = { sender };
smtp_send_message(mp->cfg, recips, 1, body, strlen(body));
free(body);
}
}
}
return imap_expunge_message(mp->cfg, msg->uid);
}
/* ---------------------------------------------------------------------- */
/* UC 4.2: Normal Broadcast Operation (maillist@domain.tld) */
/* ---------------------------------------------------------------------- */
if (!subscriber_exists(mp->subscribers, sender)) {
/* Drop unauthorized message from non-subscriber */
return imap_expunge_message(mp->cfg, msg->uid);
}
/* Build recipient list for fan-out (all subscribers) */
size_t sub_count = mp->subscribers->tree.nodecount;
if (sub_count == 0) {
return imap_expunge_message(mp->cfg, msg->uid);
}
const char **recipients = malloc(sizeof(char *) * sub_count);
size_t r_idx = 0;
splay_node_t *curr = splay_first(&mp->subscribers->tree);
while (curr != NULL) {
subscriber_entry_t *entry = (subscriber_entry_t *)curr;
recipients[r_idx++] = entry->email;
curr = splay_next(&mp->subscribers->tree, curr);
}
/* Rewrite RFC 822 headers according to E001 - E004 */
size_t outbound_len = 0;
char *outbound_msg = rebuild_message_for_broadcast(mp, msg->raw_data, &outbound_len);
if (outbound_msg) {
/* Fan-out message to all list members via SMTP */
smtp_send_message(mp->cfg, recipients, sub_count, outbound_msg, outbound_len);
free(outbound_msg);
}
free(recipients);
/* Expunge processed mail from remote IMAP inbox */
return imap_expunge_message(mp->cfg, msg->uid);
}

251
src/pending.c Normal file
View File

@ -0,0 +1,251 @@
/* pending.c - Pending subscription tracker implementation */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
#include <ilib/pending.h>
/* Case-sensitive token comparator for splay tree */
static long pending_cmp(const void *a, const void *b)
{
const char *s1 = (const char *)a;
const char *s2 = (const char *)b;
return strcmp(s1, s2);
}
static char *trim_whitespace(char *str)
{
while (isspace((unsigned char)*str)) str++;
if (*str == 0) return str;
char *end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end)) end--;
end[1] = '\0';
return str;
}
/* Generates a secure random 16-character hex token via /dev/urandom */
static void generate_token(char *buf, size_t len)
{
static const char hex[] = "0123456789abcdef";
unsigned char bytes[PENDING_ID_LEN / 2];
int fd = open("/dev/urandom", O_RDONLY);
if (fd >= 0) {
if (read(fd, bytes, sizeof(bytes)) != sizeof(bytes)) {
/* Fallback pseudo-random seeding if read fails */
for (size_t i = 0; i < sizeof(bytes); i++) bytes[i] = (unsigned char)rand();
}
close(fd);
} else {
for (size_t i = 0; i < sizeof(bytes); i++) bytes[i] = (unsigned char)rand();
}
for (size_t i = 0; i < sizeof(bytes) && (i * 2 + 1) < len; i++) {
buf[i * 2] = hex[(bytes[i] >> 4) & 0x0F];
buf[i * 2 + 1] = hex[bytes[i] & 0x0F];
}
buf[len - 1] = '\0';
}
int pending_pool_init(pending_pool_t *pool, const char *filepath, long ttl_seconds)
{
if (!pool || !filepath) return -EINVAL;
memset(pool, 0, sizeof(pending_pool_t));
pool->filepath = strdup(filepath);
if (!pool->filepath) return -ENOMEM;
pool->ttl_seconds = (ttl_seconds > 0) ? ttl_seconds : 86400; /* Default 24h */
return splay_init(&pool->tree, pending_cmp);
}
int pending_add(pending_pool_t *pool, const char *email, char *out_token, size_t token_buf_len)
{
if (!pool || !email || !out_token || token_buf_len <= PENDING_ID_LEN) return -EINVAL;
pending_purge_expired(pool);
pending_entry_t *entry = calloc(1, sizeof(pending_entry_t));
if (!entry) return -ENOMEM;
generate_token(entry->token, sizeof(entry->token));
entry->email = strdup(email);
if (!entry->email) {
free(entry);
return -ENOMEM;
}
entry->created_at = time(NULL);
int rc = splay_insert(&pool->tree, &entry->node, entry->token);
if (rc != 0) {
free(entry->email);
free(entry);
return rc;
}
snprintf(out_token, token_buf_len, "%s", entry->token);
return 0;
}
char *pending_lookup_email(pending_pool_t *pool, const char *token)
{
if (!pool || !token) return NULL;
splay_node_t *node = splay_lookup(&pool->tree, token);
if (!node) return NULL;
pending_entry_t *entry = (pending_entry_t *)node;
time_t now = time(NULL);
/* Check if token has expired past TTL */
if (now - entry->created_at > pool->ttl_seconds) {
pending_remove(pool, token);
return NULL;
}
return entry->email;
}
int pending_remove(pending_pool_t *pool, const char *token)
{
if (!pool || !token) return -EINVAL;
splay_node_t *node = splay_lookup(&pool->tree, token);
if (!node) return -ENOENT;
splay_delete(&pool->tree, node);
pending_entry_t *entry = (pending_entry_t *)node;
free(entry->email);
free(entry);
return 0;
}
void pending_purge_expired(pending_pool_t *pool)
{
if (!pool) return;
time_t now = time(NULL);
splay_node_t *curr = splay_first(&pool->tree);
while (curr != NULL) {
splay_node_t *next = splay_next(&pool->tree, curr);
pending_entry_t *entry = (pending_entry_t *)curr;
if (now - entry->created_at > pool->ttl_seconds) {
splay_delete(&pool->tree, curr);
free(entry->email);
free(entry);
}
curr = next;
}
}
int pending_load_file(pending_pool_t *pool)
{
if (!pool || !pool->filepath) return -EINVAL;
FILE *fp = fopen(pool->filepath, "r");
if (!fp) {
if (errno == ENOENT) return 0;
return -errno;
}
char line[512];
time_t now = time(NULL);
while (fgets(line, sizeof(line), fp)) {
char *str = trim_whitespace(line);
if (*str == '\0' || *str == '#') continue;
/* File line format: TOKEN TIMESTAMP EMAIL */
char *token = strtok(str, " \t");
char *ts_str = strtok(NULL, " \t");
char *email = strtok(NULL, " \t");
if (token && ts_str && email) {
time_t created_at = (time_t)atol(ts_str);
/* Skip loading entries that are already expired */
if (now - created_at > pool->ttl_seconds) continue;
pending_entry_t *entry = calloc(1, sizeof(pending_entry_t));
if (!entry) continue;
snprintf(entry->token, sizeof(entry->token), "%s", token);
entry->email = strdup(email);
entry->created_at = created_at;
if (splay_insert(&pool->tree, &entry->node, entry->token) != 0) {
free(entry->email);
free(entry);
}
}
}
fclose(fp);
return 0;
}
int pending_save_file(pending_pool_t *pool)
{
if (!pool || !pool->filepath) return -EINVAL;
pending_purge_expired(pool);
char tmp_path[1024];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp.%d", pool->filepath, getpid());
FILE *fp = fopen(tmp_path, "w");
if (!fp) return -errno;
fprintf(fp, "# Subrelay Pending Subscriptions State\n");
fprintf(fp, "# TOKEN TIMESTAMP EMAIL\n\n");
splay_node_t *curr = splay_first(&pool->tree);
while (curr != NULL) {
pending_entry_t *entry = (pending_entry_t *)curr;
fprintf(fp, "%s %ld %s\n", entry->token, (long)entry->created_at, entry->email);
curr = splay_next(&pool->tree, curr);
}
fflush(fp);
fclose(fp);
if (rename(tmp_path, pool->filepath) != 0) {
unlink(tmp_path);
return -errno;
}
return 0;
}
void pending_pool_destroy(pending_pool_t *pool)
{
if (!pool) return;
splay_node_t *curr = splay_first(&pool->tree);
while (curr != NULL) {
splay_node_t *next = splay_next(&pool->tree, curr);
splay_delete(&pool->tree, curr);
pending_entry_t *entry = (pending_entry_t *)curr;
free(entry->email);
free(entry);
curr = next;
}
if (pool->filepath) free(pool->filepath);
memset(pool, 0, sizeof(pending_pool_t));
}

308
src/splay.c Normal file
View File

@ -0,0 +1,308 @@
/* splay tree implementation */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ilib/splay.h>
static void splay_node(splay_t *, splay_node_t *);
static void tree_rotate_left(splay_node_t *, splay_node_t *);
static void tree_rotate_right(splay_node_t *, splay_node_t *);
static void right_zig_zig(splay_node_t *, splay_node_t *, splay_node_t *);
static void left_zig_zig(splay_node_t *, splay_node_t *, splay_node_t *);
static void right_zig_zag(splay_node_t *, splay_node_t *, splay_node_t *);
static void left_zig_zag(splay_node_t *, splay_node_t *, splay_node_t *);
int splay_init(splay_t *sp, splay_compare_t cmp_fu)
{
if(!sp) return EINVAL;
memset(sp, 0, sizeof(splay_t));
/* FIXME: do better maxcount */
sp->maxcount = 65535;
sp->splay_compare = cmp_fu;
/* tree init */
sp->splay_sentinel.splay_left = &sp->splay_sentinel;
sp->splay_sentinel.splay_right = &sp->splay_sentinel;
sp->splay_sentinel.splay_parent = &sp->splay_sentinel;
sp->splay_sentinel.splay_data = NULL;
sp->splay_sentinel.splay_node_key = NULL;
return 0;
}
int splay_insert(splay_t *sp, splay_node_t *node, const void *node_key)
{
splay_node_t *where = tree_root_priv(sp);
splay_node_t *nil = tree_null_priv(sp);
splay_node_t *parent = nil;
long res = -1;
node->splay_node_key = node_key;
while(where != nil) {
parent = where;
res = sp->splay_compare(node_key, where->splay_node_key);
if(!res) /* don't put duplicate */
return -EEXIST;
if(res<0) where = where->splay_left;
else where = where->splay_right;
}
if(res < 0) parent->splay_left = node;
else parent->splay_right = node;
node->splay_parent = parent;
node->splay_left = nil;
node->splay_right = nil;
sp->nodecount++;
while(node != tree_root_priv(sp))
splay_node(sp, node);
return 0;
}
int splay_delete(splay_t *sp, splay_node_t *node)
{
splay_node_t *nil = tree_null_priv(sp);
splay_node_t *child;
splay_node_t *delparent = node->splay_parent;
splay_node_t *next = node;
splay_node_t *nextparent;
if(node->splay_left != nil && node->splay_right != nil) {
next = splay_next(sp, node);
nextparent = next->splay_parent;
child = next->splay_right;
child->splay_parent = nextparent;
if(nextparent->splay_left == next)
nextparent->splay_left = child;
else {
nextparent->splay_right = child;
}
next->splay_parent = delparent;
next->splay_left = node->splay_left;
next->splay_right = node->splay_right;
next->splay_left->splay_parent = next;
next->splay_right->splay_parent = next;
if(delparent->splay_left == node) {
delparent->splay_left = next;
} else {
delparent->splay_right = next;
}
} else {
child = (node->splay_left != nil) ? node->splay_left : node->splay_right;
child->splay_parent = delparent = node->splay_parent;
if(node == delparent->splay_left) {
delparent->splay_left = child;
} else {
delparent->splay_right = child;
}
}
node->splay_parent = NULL;
node->splay_right = NULL;
node->splay_left = NULL;
sp->nodecount--;
return 0;
}
splay_node_t *splay_lookup(splay_t *sp, const void *key)
{
splay_node_t *node = NULL, *target;
splay_node_t *root = tree_root_priv(sp);
splay_node_t *nil = tree_null_priv(sp);
splay_node_t *last = nil;
long res;
while (root != nil) {
last = root;
res = sp->splay_compare(key, root->splay_node_key);
if(res < 0) root = root->splay_left;
else if(res > 0) root = root->splay_right;
else {
node = root;
break;
}
}
target = node ? node : (last != nil ? last : NULL);
if(target)
while(target != tree_root_priv(sp))
splay_node(sp, target);
return node;
}
splay_node_t *splay_first(splay_t *sp)
{
splay_node_t *nil=tree_null_priv(sp);
splay_node_t *root=tree_root_priv(sp);
splay_node_t *left;
if(root != nil)
while((left = root->splay_left) != nil)
root = left;
return (root==nil) ? NULL : root;
}
splay_node_t *splay_next(splay_t *sp, splay_node_t *curr)
{
splay_node_t *nil = tree_null_priv(sp);
splay_node_t *parent;
splay_node_t *left;
if(curr->splay_right != nil) {
curr = curr->splay_right;
while((left = curr->splay_left) != nil)
curr = left;
return curr;
}
parent = curr->splay_parent;
while((parent != nil) && (curr == parent->splay_right)) {
curr = parent;
parent = curr->splay_parent;
}
return (parent == nil) ? NULL : parent;
}
static void splay_node(splay_t *sp, splay_node_t *node)
{
splay_node_t *root = tree_root_priv(sp);
splay_node_t *parent = node->splay_parent;
if(parent->splay_left == node) {
if(parent == root) {
tree_rotate_right(node, parent);
} else {
splay_node_t *grandpa = parent->splay_parent;
if(grandpa->splay_left == parent)
right_zig_zig(node, parent, grandpa);
else {
if(grandpa->splay_right != parent) return;
right_zig_zag(node, parent, grandpa);
}
}
} else {
if(parent->splay_right != node)
return;
if(parent == root) {
tree_rotate_left(node, parent);
} else {
splay_node_t *grandpa = parent->splay_parent;
if(grandpa->splay_right == parent)
left_zig_zig(node, parent, grandpa);
else {
if(grandpa->splay_left != parent) return;
left_zig_zag(node, parent, grandpa);
}
}
}
return;
}
static void tree_rotate_left(splay_node_t *child, splay_node_t *parent)
{
splay_node_t *leftgrandchild;
splay_node_t *grandpa;
if(parent->splay_right != child) return;
child = parent->splay_right;
parent->splay_right = leftgrandchild = child->splay_left;
leftgrandchild->splay_parent = parent;
child->splay_parent = grandpa = parent->splay_parent;
if(parent == grandpa->splay_left) grandpa->splay_left=child;
else {
if(parent != grandpa->splay_right)
return;
grandpa->splay_right = child;
}
child->splay_left = parent;
parent->splay_parent = child;
return;
}
static void tree_rotate_right(splay_node_t *child, splay_node_t *parent)
{
splay_node_t *rightgrandchild;
splay_node_t *grandpa;
if(parent->splay_left != child) return;
parent->splay_left = rightgrandchild = child->splay_right;
rightgrandchild->splay_parent = parent;
child->splay_parent = grandpa = parent->splay_parent;
if(parent == grandpa->splay_right) grandpa->splay_right = child;
else {
if(parent != grandpa->splay_left)
return;
grandpa->splay_left = child;
}
child->splay_right = parent;
parent->splay_parent = child;
return;
}
static void right_zig_zig(splay_node_t *child, splay_node_t *parent, splay_node_t *grandpa)
{
tree_rotate_right(parent, grandpa);
tree_rotate_right(child, parent);
return;
}
static void left_zig_zig(splay_node_t *child, splay_node_t *parent, splay_node_t *grandpa)
{
tree_rotate_left(parent, grandpa);
tree_rotate_left(child, parent);
return;
}
static void right_zig_zag(splay_node_t *child, splay_node_t *parent, splay_node_t *grandpa)
{
tree_rotate_right(child, parent);
tree_rotate_left(child, grandpa);
return;
}
static void left_zig_zag(splay_node_t *child, splay_node_t *parent, splay_node_t *grandpa)
{
tree_rotate_left(child, parent);
tree_rotate_right(child, grandpa);
return;
}

86
src/subrelay-shell.c Normal file
View File

@ -0,0 +1,86 @@
/* src/subrelay-shell/main.c - Management CLI tool */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
static void print_usage(const char *progname)
{
printf("Subrelay Management Shell\n");
printf("Usage: %s -s <socket_path> <command> [args...]\n\n", progname);
printf("Commands:\n");
printf(" ADD <email> Directly subscribe an email address\n");
printf(" DEL <email> Unsubscribe an email address\n");
printf(" LIST List all current subscribers\n");
printf(" STATUS Show daemon operational status\n");
}
int main(int argc, char **argv)
{
char *socket_path = "/var/run/subrelay/subrelayd.sock";
int opt;
while ((opt = getopt(argc, argv, "s:h")) != -1) {
switch (opt) {
case 's': socket_path = optarg; break;
case 'h': print_usage(argv[0]); return EXIT_SUCCESS;
default: print_usage(argv[0]); return EXIT_FAILURE;
}
}
if (optind >= argc) {
fprintf(stderr, "error: command required\n");
print_usage(argv[0]);
return EXIT_FAILURE;
}
/* Build command string from remaining arguments */
char cmd_buf[1024];
cmd_buf[0] = '\0';
for (int i = optind; i < argc; i++) {
strcat(cmd_buf, argv[i]);
if (i < argc - 1) strcat(cmd_buf, " ");
}
strcat(cmd_buf, "\n");
/* Connect to subrelayd UNIX domain socket */
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
perror("error opening socket");
return EXIT_FAILURE;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
fprintf(stderr, "error: cannot connect to subrelayd at '%s': %s\n",
socket_path, strerror(errno));
close(fd);
return EXIT_FAILURE;
}
/* Send command */
if (write(fd, cmd_buf, strlen(cmd_buf)) < 0) {
perror("error writing command to socket");
close(fd);
return EXIT_FAILURE;
}
/* Read and print daemon response */
char resp[2048];
ssize_t bytes;
while ((bytes = read(fd, resp, sizeof(resp) - 1)) > 0) {
resp[bytes] = '\0';
printf("%s", resp);
}
close(fd);
return EXIT_SUCCESS;
}

204
src/subrelayd.c Normal file
View File

@ -0,0 +1,204 @@
/* src/subrelayd/main.c - Main daemon process */
#define _POSIX_C_SOURCE 200809L
#define _BSD_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <getopt.h>
#include <errno.h>
#include <pwd.h>
#include <grp.h>
#include <ilib/config.h>
#include <ilib/subscribers.h>
#include <ilib/pending.h>
#include <ilib/mail_engine.h>
#include <ilib/mail_processor.h>
#include <ilib/ipc.h>
static volatile int running = 1;
static void handle_signal(int sig)
{
(void)sig;
running = 0;
}
/* Drops root privileges to specified user and group */
static int drop_privileges(const char *username, const char *groupname)
{
if (getuid() != 0) return 0; /* Not running as root, skip */
if (groupname && strlen(groupname) > 0) {
struct group *gr = getgrnam(groupname);
if (!gr) {
fprintf(stderr, "error: unknown group '%s'\n", groupname);
return -EINVAL;
}
if (setgid(gr->gr_gid) != 0) {
fprintf(stderr, "error: failed to setgid to '%s'\n", groupname);
return -errno;
}
}
if (username && strlen(username) > 0) {
struct passwd *pw = getpwnam(username);
if (!pw) {
fprintf(stderr, "error: unknown user '%s'\n", username);
return -EINVAL;
}
if (setuid(pw->pw_uid) != 0) {
fprintf(stderr, "error: failed to setuid to '%s'\n", username);
return -errno;
}
}
return 0;
}
static void print_usage(const char *progname)
{
printf("Usage: %s -c <config_file> [-d]\n", progname);
printf(" -c <path> Path to subrelay configuration file\n");
printf(" -d Run in foreground/debug mode\n");
}
int main(int argc, char **argv)
{
char *config_path = NULL;
int debug_mode = 0;
int opt;
while ((opt = getopt(argc, argv, "c:d")) != -1) {
switch (opt) {
case 'c': config_path = optarg; break;
case 'd': debug_mode = 1; break;
default: print_usage(argv[0]); return EXIT_FAILURE;
}
}
if (!config_path) {
fprintf(stderr, "error: configuration file required (-c <config_path>)\n");
print_usage(argv[0]);
return EXIT_FAILURE;
}
/* Signal handlers for clean shutdown */
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handle_signal;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
/* 1. Load Configuration */
subrelay_config_t cfg;
if (config_load(&cfg, config_path) != 0) {
fprintf(stderr, "error: failed to load config file '%s'\n", config_path);
return EXIT_FAILURE;
}
/* 2. Initialize In-Memory Splay Pools & Storage */
subscriber_pool_t subscribers;
if (subscriber_pool_init(&subscribers, cfg.subscribers_file) != 0) {
fprintf(stderr, "error: failed to initialize subscriber pool\n");
config_free(&cfg);
return EXIT_FAILURE;
}
subscriber_load_file(&subscribers);
char pending_path[1024];
snprintf(pending_path, sizeof(pending_path), "%s.pending", cfg.subscribers_file);
pending_pool_t pending;
if (pending_pool_init(&pending, pending_path, 86400) != 0) {
fprintf(stderr, "error: failed to initialize pending pool\n");
subscriber_pool_destroy(&subscribers);
config_free(&cfg);
return EXIT_FAILURE;
}
pending_load_file(&pending);
/* 3. Initialize IPC Socket Server */
ipc_server_t ipc;
if (ipc_server_init(&ipc, &cfg, &subscribers, &pending) != 0) {
fprintf(stderr, "error: failed to bind IPC socket '%s'\n", cfg.socket_path);
pending_pool_destroy(&pending);
subscriber_pool_destroy(&subscribers);
config_free(&cfg);
return EXIT_FAILURE;
}
/* 4. Drop Root Privileges */
if (drop_privileges(cfg.run_user, cfg.run_group) != 0) {
fprintf(stderr, "error: failed to drop privileges\n");
ipc_server_destroy(&ipc);
pending_pool_destroy(&pending);
subscriber_pool_destroy(&subscribers);
config_free(&cfg);
return EXIT_FAILURE;
}
/* 5. Initialize Network Engine */
if (mail_engine_global_init() != 0) {
fprintf(stderr, "error: failed to initialize libcurl global context\n");
ipc_server_destroy(&ipc);
pending_pool_destroy(&pending);
subscriber_pool_destroy(&subscribers);
config_free(&cfg);
return EXIT_FAILURE;
}
mail_processor_t processor;
mail_processor_init(&processor, &cfg, &subscribers, &pending);
if (!debug_mode) {
if (daemon(0, 0) != 0) {
fprintf(stderr, "error: failed to daemonize\n");
return EXIT_FAILURE;
}
}
printf("subrelayd started. Managing list '%s' <%s>\n", cfg.maillist_name, cfg.maillist_addr);
/* 6. Main Operational Daemon Loop */
while (running) {
/* Check local UNIX socket for subrelay-shell IPC commands */
ipc_server_poll(&ipc);
/* Poll remote IMAP server for unseen messages */
mail_list_results_t unseen;
if (imap_fetch_unseen_uids(&cfg, &unseen) == 0) {
for (size_t i = 0; i < unseen.count && running; i++) {
mail_message_t msg;
if (imap_fetch_message(&cfg, unseen.uids[i], &msg) == 0) {
/* Execute UC 4.2 - 4.5 message routing */
mail_processor_execute(&processor, &msg);
mail_message_free(&msg);
}
}
mail_list_results_free(&unseen);
}
/* Periodic cleanup of stale double opt-in tokens */
pending_purge_expired(&pending);
/* Sleep 5 seconds between IMAP poll cycles */
sleep(5);
}
printf("\nsubrelayd shutting down cleanly...\n");
/* Cleanup & Flush Persistence */
subscriber_save_file(&subscribers);
pending_save_file(&pending);
mail_engine_global_cleanup();
ipc_server_destroy(&ipc);
pending_pool_destroy(&pending);
subscriber_pool_destroy(&subscribers);
config_free(&cfg);
return EXIT_SUCCESS;
}

174
src/subscribers.c Normal file
View File

@ -0,0 +1,174 @@
/* subscribers.c - Subscriber pool implementation */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <ctype.h>
#include <unistd.h>
#include <ilib/subscribers.h>
/* Case-insensitive string comparison for email addresses */
static long subscriber_cmp(const void *a, const void *b)
{
const char *s1 = (const char *)a;
const char *s2 = (const char *)b;
return strcasecmp(s1, s2);
}
/* Helper to trim leading/trailing whitespace and newlines */
static char *trim_whitespace(char *str)
{
char *end;
while (isspace((unsigned char)*str)) str++;
if (*str == 0) return str;
end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end)) end--;
end[1] = '\0';
return str;
}
int subscriber_pool_init(subscriber_pool_t *pool, const char *filepath)
{
if (!pool || !filepath) return -EINVAL;
memset(pool, 0, sizeof(subscriber_pool_t));
pool->filepath = strdup(filepath);
if (!pool->filepath) return -ENOMEM;
return splay_init(&pool->tree, subscriber_cmp);
}
int subscriber_add(subscriber_pool_t *pool, const char *email)
{
if (!pool || !email || strlen(email) == 0) return -EINVAL;
/* Allocate container entry */
subscriber_entry_t *entry = calloc(1, sizeof(subscriber_entry_t));
if (!entry) return -ENOMEM;
entry->email = strdup(email);
if (!entry->email) {
free(entry);
return -ENOMEM;
}
/* Insert into splay tree using normalized email as key */
int rc = splay_insert(&pool->tree, &entry->node, entry->email);
if (rc != 0) {
/* Entry already exists or insertion error */
free(entry->email);
free(entry);
return rc;
}
return 0;
}
int subscriber_remove(subscriber_pool_t *pool, const char *email)
{
if (!pool || !email) return -EINVAL;
splay_node_t *node = splay_lookup(&pool->tree, email);
if (!node) return -ENOENT;
splay_delete(&pool->tree, node);
/* Retrieve wrapping entry struct and release memory */
subscriber_entry_t *entry = (subscriber_entry_t *)node;
free(entry->email);
free(entry);
return 0;
}
int subscriber_exists(subscriber_pool_t *pool, const char *email)
{
if (!pool || !email) return 0;
return (splay_lookup(&pool->tree, email) != NULL);
}
int subscriber_load_file(subscriber_pool_t *pool)
{
if (!pool || !pool->filepath) return -EINVAL;
FILE *fp = fopen(pool->filepath, "r");
if (!fp) {
/* If file doesn't exist yet, it will be created on first save */
if (errno == ENOENT) return 0;
return -errno;
}
char line[512];
while (fgets(line, sizeof(line), fp)) {
char *email = trim_whitespace(line);
/* Ignore empty lines and comment lines starting with '#' */
if (*email == '\0' || *email == '#') continue;
subscriber_add(pool, email);
}
fclose(fp);
return 0;
}
/* Atomic save using temporary file + rename to prevent corruption */
int subscriber_save_file(subscriber_pool_t *pool)
{
if (!pool || !pool->filepath) return -EINVAL;
char tmp_path[1024];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp.%d", pool->filepath, getpid());
FILE *fp = fopen(tmp_path, "w");
if (!fp) return -errno;
/* Write plain-text header */
fprintf(fp, "# Subrelay Subscribers List\n");
fprintf(fp, "# Do not edit manually while subrelayd is running\n\n");
/* In-order traversal across splay tree nodes */
splay_node_t *curr = splay_first(&pool->tree);
while (curr != NULL) {
subscriber_entry_t *entry = (subscriber_entry_t *)curr;
fprintf(fp, "%s\n", entry->email);
curr = splay_next(&pool->tree, curr);
}
fflush(fp);
fclose(fp);
/* Atomic swap */
if (rename(tmp_path, pool->filepath) != 0) {
unlink(tmp_path);
return -errno;
}
return 0;
}
void subscriber_pool_destroy(subscriber_pool_t *pool)
{
if (!pool) return;
/* Traverse tree and free all allocated nodes */
splay_node_t *curr = splay_first(&pool->tree);
while (curr != NULL) {
splay_node_t *next = splay_next(&pool->tree, curr);
splay_delete(&pool->tree, curr);
subscriber_entry_t *entry = (subscriber_entry_t *)curr;
free(entry->email);
free(entry);
curr = next;
}
if (pool->filepath) free(pool->filepath);
memset(pool, 0, sizeof(subscriber_pool_t));
return;
}

122
src/template.c Normal file
View File

@ -0,0 +1,122 @@
/* template.c - Template substitution implementation */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <ilib/template.h>
/* Helper to check if a character can be part of a variable name ($KEY) */
static int is_var_char(char c)
{
return isalnum((unsigned char)c) || c == '_';
}
char *template_render_string(const char *src, const template_var_t *vars, size_t var_count)
{
if (!src) return NULL;
/* Start with a reasonable initial buffer size */
size_t buf_cap = strlen(src) * 2 + 128;
size_t buf_len = 0;
char *out = malloc(buf_cap);
if (!out) return NULL;
const char *p = src;
while (*p) {
if (*p == '$') {
const char *var_start = p + 1;
const char *var_end = var_start;
/* Parse contiguous alphanumeric/underscore variable name */
while (*var_end && is_var_char(*var_end)) {
var_end++;
}
size_t key_len = var_end - var_start;
if (key_len > 0) {
const char *replacement = NULL;
/* Search variable map for matching key */
for (size_t i = 0; i < var_count; i++) {
if (vars[i].key && strncmp(vars[i].key, var_start, key_len) == 0 &&
vars[i].key[key_len] == '\0') {
replacement = vars[i].val ? vars[i].val : "";
break;
}
}
if (replacement) {
size_t rep_len = strlen(replacement);
/* Expand output buffer if replacement exceeds capacity */
while (buf_len + rep_len >= buf_cap) {
buf_cap *= 2;
char *tmp = realloc(out, buf_cap);
if (!tmp) {
free(out);
return NULL;
}
out = tmp;
}
memcpy(out + buf_len, replacement, rep_len);
buf_len += rep_len;
p = var_end; /* Advance past $KEY */
continue;
}
}
}
/* Standard character copy */
if (buf_len + 1 >= buf_cap) {
buf_cap *= 2;
char *tmp = realloc(out, buf_cap);
if (!tmp) {
free(out);
return NULL;
}
out = tmp;
}
out[buf_len++] = *p++;
}
out[buf_len] = '\0';
return out;
}
char *template_render_file(const char *template_path, const template_var_t *vars, size_t var_count)
{
if (!template_path) return NULL;
FILE *fp = fopen(template_path, "rb");
if (!fp) return NULL;
fseek(fp, 0, SEEK_END);
long filesize = ftell(fp);
fseek(fp, 0, SEEK_SET);
if (filesize < 0) {
fclose(fp);
return NULL;
}
char *src = malloc(filesize + 1);
if (!src) {
fclose(fp);
return NULL;
}
size_t read_bytes = fread(src, 1, filesize, fp);
fclose(fp);
src[read_bytes] = '\0';
char *rendered = template_render_string(src, vars, var_count);
free(src);
return rendered;
}