From 007c81eebed440b8365b5d3ce0f2a8bae3d763f7 Mon Sep 17 00:00:00 2001 From: Alexander Vdolainen Date: Tue, 29 Sep 2026 01:28:53 +0300 Subject: [PATCH] verion 0.0.1 sources added; --- include/ilib/config.h | 23 ++ include/ilib/ipc.h | 29 +++ include/ilib/mail_engine.h | 38 +++ include/ilib/mail_processor.h | 24 ++ include/ilib/pending.h | 47 ++++ include/ilib/splay.h | 36 +++ include/ilib/subscribers.h | 28 ++ include/ilib/template.h | 27 ++ src/Makefile.am | 39 +++ src/config.c | 138 ++++++++++ src/ipc.c | 195 ++++++++++++++ src/mail_engine.c | 239 +++++++++++++++++ src/mail_processor.c | 466 ++++++++++++++++++++++++++++++++++ src/pending.c | 251 ++++++++++++++++++ src/splay.c | 308 ++++++++++++++++++++++ src/subrelay-shell.c | 86 +++++++ src/subrelayd.c | 204 +++++++++++++++ src/subscribers.c | 174 +++++++++++++ src/template.c | 122 +++++++++ 19 files changed, 2474 insertions(+) create mode 100644 include/ilib/config.h create mode 100644 include/ilib/ipc.h create mode 100644 include/ilib/mail_engine.h create mode 100644 include/ilib/mail_processor.h create mode 100644 include/ilib/pending.h create mode 100644 include/ilib/splay.h create mode 100644 include/ilib/subscribers.h create mode 100644 include/ilib/template.h create mode 100644 src/Makefile.am create mode 100644 src/config.c create mode 100644 src/ipc.c create mode 100644 src/mail_engine.c create mode 100644 src/mail_processor.c create mode 100644 src/pending.c create mode 100644 src/splay.c create mode 100644 src/subrelay-shell.c create mode 100644 src/subrelayd.c create mode 100644 src/subscribers.c create mode 100644 src/template.c diff --git a/include/ilib/config.h b/include/ilib/config.h new file mode 100644 index 0000000..b48a638 --- /dev/null +++ b/include/ilib/config.h @@ -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__ */ diff --git a/include/ilib/ipc.h b/include/ilib/ipc.h new file mode 100644 index 0000000..d09df55 --- /dev/null +++ b/include/ilib/ipc.h @@ -0,0 +1,29 @@ +/* ipc.h - UNIX domain socket IPC listener and command processor */ +#ifndef __IPC_H__ +#define __IPC_H__ + +#include +#include +#include + +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__ */ diff --git a/include/ilib/mail_engine.h b/include/ilib/mail_engine.h new file mode 100644 index 0000000..f41c1c5 --- /dev/null +++ b/include/ilib/mail_engine.h @@ -0,0 +1,38 @@ +/* mail_engine.h - IMAP fetching & SMTP fan-out engine using libcurl */ +#ifndef __MAIL_ENGINE_H__ +#define __MAIL_ENGINE_H__ + +#include + +#include + +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__ */ diff --git a/include/ilib/mail_processor.h b/include/ilib/mail_processor.h new file mode 100644 index 0000000..41d2f6a --- /dev/null +++ b/include/ilib/mail_processor.h @@ -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 +#include +#include +#include + +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__ */ diff --git a/include/ilib/pending.h b/include/ilib/pending.h new file mode 100644 index 0000000..7ca1317 --- /dev/null +++ b/include/ilib/pending.h @@ -0,0 +1,47 @@ +/* pending.h - Pending subscription state tracker using splay tree */ +#ifndef __PENDING_H__ +#define __PENDING_H__ + +#include +#include + +#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__ */ diff --git a/include/ilib/splay.h b/include/ilib/splay.h new file mode 100644 index 0000000..442bda9 --- /dev/null +++ b/include/ilib/splay.h @@ -0,0 +1,36 @@ +/* splay tree implementation */ +#ifndef __SPLAY_H__ +#define __SPLAY_H__ + +#include + +#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__*/ diff --git a/include/ilib/subscribers.h b/include/ilib/subscribers.h new file mode 100644 index 0000000..c624eaf --- /dev/null +++ b/include/ilib/subscribers.h @@ -0,0 +1,28 @@ +/* subscribers.h - Subscriber pool & persistence management */ +#ifndef __SUBSCRIBERS_H__ +#define __SUBSCRIBERS_H__ + +#include + +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__ */ diff --git a/include/ilib/template.h b/include/ilib/template.h new file mode 100644 index 0000000..853fd40 --- /dev/null +++ b/include/ilib/template.h @@ -0,0 +1,27 @@ +/* template.h - Template parser and string variable substitution engine */ +#ifndef __TEMPLATE_H__ +#define __TEMPLATE_H__ + +#include + +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__ */ diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..dff0637 --- /dev/null +++ b/src/Makefile.am @@ -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 diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..e26180a --- /dev/null +++ b/src/config.c @@ -0,0 +1,138 @@ +/* config.c - Key-value configuration parser for subrelayd */ +#include +#include +#include +#include +#include + +#include + +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: : '' */ + 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)); +} diff --git a/src/ipc.c b/src/ipc.c new file mode 100644 index 0000000..8bfe96f --- /dev/null +++ b/src/ipc.c @@ -0,0 +1,195 @@ +/* ipc.c - UNIX domain socket ASCII command listener */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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 (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 (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)); +} diff --git a/src/mail_engine.c b/src/mail_engine.c new file mode 100644 index 0000000..407c239 --- /dev/null +++ b/src/mail_engine.c @@ -0,0 +1,239 @@ +/* mail_engine.c - libcurl IMAP and SMTP implementation */ +#include +#include +#include +#include +#include + +#include + +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)); + } +} diff --git a/src/mail_processor.c b/src/mail_processor.c new file mode 100644 index 0000000..dc8b915 --- /dev/null +++ b/src/mail_processor.c @@ -0,0 +1,466 @@ +/* mail_processor.c - RFC 822 header parsing & routing engine */ +#include +#include +#include +#include +#include +#include + +#include +#include + +/* 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 " */ +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: \r\n" + "List-Post: \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+@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); +} diff --git a/src/pending.c b/src/pending.c new file mode 100644 index 0000000..a3980c0 --- /dev/null +++ b/src/pending.c @@ -0,0 +1,251 @@ +/* pending.c - Pending subscription tracker implementation */ +#include +#include +#include +#include +#include +#include +#include + +#include + +/* 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)); +} diff --git a/src/splay.c b/src/splay.c new file mode 100644 index 0000000..6875bf6 --- /dev/null +++ b/src/splay.c @@ -0,0 +1,308 @@ +/* splay tree implementation */ + +#include +#include +#include +#include + +#include + +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; +} diff --git a/src/subrelay-shell.c b/src/subrelay-shell.c new file mode 100644 index 0000000..085c5ce --- /dev/null +++ b/src/subrelay-shell.c @@ -0,0 +1,86 @@ +/* src/subrelay-shell/main.c - Management CLI tool */ +#include +#include +#include +#include +#include +#include +#include + +static void print_usage(const char *progname) +{ + printf("Subrelay Management Shell\n"); + printf("Usage: %s -s [args...]\n\n", progname); + printf("Commands:\n"); + printf(" ADD Directly subscribe an email address\n"); + printf(" DEL 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; +} diff --git a/src/subrelayd.c b/src/subrelayd.c new file mode 100644 index 0000000..9644588 --- /dev/null +++ b/src/subrelayd.c @@ -0,0 +1,204 @@ +/* src/subrelayd/main.c - Main daemon process */ +#define _POSIX_C_SOURCE 200809L +#define _BSD_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +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 [-d]\n", progname); + printf(" -c 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 )\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; +} diff --git a/src/subscribers.c b/src/subscribers.c new file mode 100644 index 0000000..a9e7398 --- /dev/null +++ b/src/subscribers.c @@ -0,0 +1,174 @@ +/* subscribers.c - Subscriber pool implementation */ +#include +#include +#include +#include +#include +#include +#include + +#include + +/* 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; +} diff --git a/src/template.c b/src/template.c new file mode 100644 index 0000000..5b003b2 --- /dev/null +++ b/src/template.c @@ -0,0 +1,122 @@ +/* template.c - Template substitution implementation */ +#include +#include +#include +#include +#include + +#include + +/* 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; +}