verion 0.0.1 sources added;
This commit is contained in:
parent
edea3115bc
commit
007c81eebe
23
include/ilib/config.h
Normal file
23
include/ilib/config.h
Normal 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
29
include/ilib/ipc.h
Normal 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__ */
|
||||
38
include/ilib/mail_engine.h
Normal file
38
include/ilib/mail_engine.h
Normal 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__ */
|
||||
24
include/ilib/mail_processor.h
Normal file
24
include/ilib/mail_processor.h
Normal 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
47
include/ilib/pending.h
Normal 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
36
include/ilib/splay.h
Normal 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__*/
|
||||
28
include/ilib/subscribers.h
Normal file
28
include/ilib/subscribers.h
Normal 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
27
include/ilib/template.h
Normal 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
39
src/Makefile.am
Normal 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
138
src/config.c
Normal 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
195
src/ipc.c
Normal 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
239
src/mail_engine.c
Normal 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
466
src/mail_processor.c
Normal 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
251
src/pending.c
Normal 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
308
src/splay.c
Normal 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
86
src/subrelay-shell.c
Normal 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
204
src/subrelayd.c
Normal 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
174
src/subscribers.c
Normal 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
122
src/template.c
Normal 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user