diff --git a/include/ilib/mail_processor.h b/include/ilib/mail_processor.h
index 69f2d91..fbbbcbd 100644
--- a/include/ilib/mail_processor.h
+++ b/include/ilib/mail_processor.h
@@ -1,4 +1,23 @@
-/* mail_processor.h - Header parsing, header rewriting, and UC 4.2-4.5 routing */
+/*
+ * this is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published
+ * by the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * this is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .";
+ *
+ * (c) Alexander Vdolainen 2007, 2026
+ * subrelay: a minimalistic maillist server based on external IMAPs/SMTPs
+ *
+ * mail_processor.h: RFC 822/RFC 5322 header parsing/rewriting and routing
+ *
+ */
#ifndef __MAIL_PROCESSOR_H__
#define __MAIL_PROCESSOR_H__
@@ -10,15 +29,13 @@
typedef struct mail_processor {
subrelay_config_t *cfg;
subscriber_pool_t *subscribers;
- pending_pool_t *pending;
+ 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);
+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 */
+/* Processes a single fetched message according to defined use cases */
int mail_processor_execute(mail_processor_t *mp, mail_message_t *msg);
#endif /* __MAIL_PROCESSOR_H__ */
diff --git a/include/ilib/misc.h b/include/ilib/misc.h
index a5f0717..525926a 100644
--- a/include/ilib/misc.h
+++ b/include/ilib/misc.h
@@ -27,5 +27,23 @@
/* Build time constants */
#define PENDING_ID_LEN 16
#define DEFAULT_PENDING_TTL 86400 /* default TTL for confirmation email */
+#define EMAILLEN_MAX 256 /* Max length of the email address */
+#define TAGLEN_MAX 512 /* Max length of the tag subject in broadcast email */
+
+/* message builder -- a safe growing buffer for message rebuilds */
+typedef struct {
+ char *data;
+ size_t len;
+ size_t cap;
+} message_builder_t;
+
+/* MB functions */
+int mb_reserve(message_builder_t *b, size_t additional);
+int mb_append(message_builder_t *b, const void *data, size_t len);
+int mb_append_cstr(message_builder_t *b, const char *s);
+
+/* helper functions */
+/* Trims whitespace and trailing \r\n */
+char *strtrim_whitespace(char *str);
#endif /* __MISC_H__ */
diff --git a/src/Makefile.am b/src/Makefile.am
index 75fdacd..64234d6 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -15,6 +15,7 @@ noinst_HEADERS = \
subrelayd_SOURCES = \
subrelayd.c \
splay.c \
+ misc.c \
subscribers.c \
pending.c \
configuration.c \
diff --git a/src/configuration.c b/src/configuration.c
index 4874765..2ad5be8 100644
--- a/src/configuration.c
+++ b/src/configuration.c
@@ -24,26 +24,15 @@
#include
#include
+#include
#include
#define LINE_MAXLEN 1024
-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);
+ val = strtrim_whitespace(val);
size_t len = strlen(val);
if (len >= 2 && ((val[0] == '\'' && val[len - 1] == '\'') ||
@@ -60,7 +49,7 @@ static int text2port(const char *value, int *port)
errno = 0;
long n = strtol(value, &end, 10);
- if (value == end || errno == ERANGE || *trim(end) != '\0' ||
+ if (value == end || errno == ERANGE || *strtrim_whitespace(end) != '\0' ||
n < 1 || n > 65535)
return -EINVAL;
@@ -101,7 +90,7 @@ int configuration_load(subrelay_config_t *cfg, const char *filepath)
char *comment = strchr(line, '#');
if (comment) *comment = '\0';
- char *trimmed = trim(line);
+ char *trimmed = strtrim_whitespace(line);
if (*trimmed == '\0') continue; /* skip empty/comment lines */
/* requirement C004.2: : '' */
@@ -112,7 +101,7 @@ int configuration_load(subrelay_config_t *cfg, const char *filepath)
}
*colon = '\0';
- char *key = trim(trimmed);
+ char *key = strtrim_whitespace(trimmed);
char *value = strip_quotes(colon + 1);
if (strcasecmp(key, "MAILLIST_ADDR") == 0) {
diff --git a/src/local.c b/src/local.c
index bd81878..5da482b 100644
--- a/src/local.c
+++ b/src/local.c
@@ -30,27 +30,15 @@
#include
#include
+#include
#include
#define OUTBUF_MAX 2048
-#define EMAILLEN_MAX 512
-
-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;
-}
/* dispatch requests from local control tool */
static void handle_ipc_command(local_ctrl_t *server, int client_fd, char *cmd_line)
{
- char *line = trim(cmd_line);
+ char *line = strtrim_whitespace(cmd_line);
if (*line == '\0') return;
char *cmd = strtok(line, " \t");
diff --git a/src/mail_processor.c b/src/mail_processor.c
index dc8b915..4975edf 100644
--- a/src/mail_processor.c
+++ b/src/mail_processor.c
@@ -1,4 +1,24 @@
-/* mail_processor.c - RFC 822 header parsing & routing engine */
+/*
+ * this is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published
+ * by the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * this is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .";
+ *
+ * (c) Alexander Vdolainen 2007, 2026
+ * subrelay: a minimalistic maillist server based on external IMAPs/SMTPs
+ *
+ * mail_processor.c: RFC 822/RFC 5322 header parsing/rewriting and routing
+ *
+ */
+
#include
#include
#include
@@ -6,272 +26,509 @@
#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)
+static int extract_email_address(const char *raw, char *out, size_t out_len)
{
- const char *start = strchr(raw, '<');
- const char *end = strchr(raw, '>');
+ if (!raw || !out || out_len == 0) return -1;
- if (start && end && end > start) {
- size_t len = end - start - 1;
- if (len >= out_len) len = out_len - 1;
- strncpy(out, start + 1, len);
+ const char *start = strchr(raw, '<');
+ const char *end = start ? strchr(start + 1, '>') : NULL;
+
+ if (start && end) {
+ raw = start + 1;
+ size_t len = (size_t)(end - raw);
+ if (len == 0 || len >= out_len) return -1;
+
+ memcpy(out, raw, len);
out[len] = '\0';
} else {
- snprintf(out, out_len, "%s", raw);
+ size_t len = strlen(raw);
+ if (len >= out_len) return -1;
+
+ memcpy(out, raw, len + 1);
}
- trim(out);
+
+ strtrim_whitespace(out);
+
+ if (out[0] == '\0') return -1;
+ return 0;
}
/* 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);
+ if (!raw_msg || !header_name || !*header_name) return NULL;
+
+ const size_t name_len = strlen(header_name);
const char *p = raw_msg;
+ char *value = NULL;
+ size_t value_len = 0;
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;
+ const char *line = p;
+ const char *lf = strchr(p, '\n');
+ const char *end = lf ? lf : p + strlen(p);
- if (strncasecmp(p, header_name, hlen) == 0 && p[hlen] == ':') {
- p += hlen + 1;
- while (*p == ' ' || *p == '\t') p++;
+ if (end > line && end[-1] == '\r') end--;
- const char *val_start = p;
- while (*p && *p != '\r' && *p != '\n') p++;
+ /* Empty line marks the end of the header block. */
+ if (end == line) break;
- size_t val_len = p - val_start;
- char *val = malloc(val_len + 1);
- if (!val) return NULL;
+ if (*line == ' ' || *line == '\t') {
+ /* Continuation belongs to the preceding header. */
+ if (value) {
+ const char *q = line;
+ while (q < end && (*q == ' ' || *q == '\t')) q++;
- memcpy(val, val_start, val_len);
- val[val_len] = '\0';
+ size_t add = (size_t)(end - q);
+ char *tmp = realloc(value, value_len + 1 + add + 1);
+ if (!tmp) {
+ free(value);
+ return NULL;
+ }
- return trim(val);
+ value = tmp;
+ value[value_len++] = ' ';
+ memcpy(value + value_len, q, add);
+ value_len += add;
+ value[value_len] = '\0';
+ }
+ } else {
+ /* A new header ends the value we were collecting. */
+ if (value) break;
+
+ const char *colon = memchr(line, ':', (size_t)(end - line));
+ if (colon && (size_t)(colon - line) == name_len &&
+ strncasecmp(line, header_name, name_len) == 0) {
+ const char *v = colon + 1;
+ while (v < end && (*v == ' ' || *v == '\t')) v++;
+
+ value_len = (size_t)(end - v);
+ value = malloc(value_len + 1);
+ if (!value) return NULL;
+
+ memcpy(value, v, value_len);
+ value[value_len] = '\0';
+ }
}
- /* Move to next line */
- while (*p && *p != '\n') p++;
- if (*p == '\n') p++;
+ p = lf ? lf + 1 : end;
+ if (!lf) break;
}
- return NULL;
+ if (value) strtrim_whitespace(value);
+ return value;
}
-/* Checks if a header key should be stripped (E002 & E004) */
-static int should_strip_header(const char *line)
+/* Reject values that could inject additional message headers. */
+static int header_value_is_safe(const char *s)
{
- /* Advance past any leading CRLF/whitespace */
- while (*line == '\r' || *line == '\n' || *line == ' ' || *line == '\t') line++;
+ if (!s) return 0;
- 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 (; *s; s++) {
+ unsigned char c = (unsigned char)*s;
+ if (c == '\r' || c == '\n' || c == 0x7f ||
+ (c < 0x20 && c != '\t'))
+ return 0;
+ }
- for (int i = 0; strip_list[i] != NULL; i++) {
- if (strncasecmp(line, strip_list[i], strlen(strip_list[i])) == 0)
+ return 1;
+}
+
+static int field_name_eq(const char *name, size_t name_len, const char *wanted)
+{
+ return strlen(wanted) == name_len &&
+ strncasecmp(name, wanted, name_len) == 0;
+}
+
+/* Checks if a header key should be stripped */
+static int should_strip_header(const char *line, size_t line_len)
+{
+ static const char *const exact[] = {
+ "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",
+ NULL};
+
+ for (size_t i = 0; exact[i]; i++) {
+ if (field_name_eq(line, line_len, exact[i]))
return 1;
}
+
+ static const char *const prefix[] = {
+ "X-Spam-", "X-Google-", "X-Zoho-", "X-Gm-", "ARC-",
+ NULL};
+
+ for (size_t i = 0; prefix[i]; i++) {
+ size_t n = strlen(prefix[i]);
+ if(line_len >= n && strncasecmp(line, prefix[i], n) == 0)
+ return 1;
+ }
+
return 0;
}
static int is_header_continuation(const char *line)
{
- return (line[0] == ' ' || line[0] == '\t');
+ return line && (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)
+/*
+ * Locate the earliest CRLF-CRLF or LF-LF separator. (RFC 822)
+ * Returns the start of the body and reports the separator length.
+ * If there is no separator, the body is considered empty.
+ */
+static const char *find_body_start(const char *msg, size_t *separator_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);
+ const char *crlf = strstr(msg, "\r\n\r\n");
+ const char *lf = strstr(msg, "\n\n");
+
+ if (crlf && (!lf || crlf <= lf)) {
+ *separator_len = 4;
+ return crlf + 4;
}
- /* 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);
+ if (lf) {
+ *separator_len = 2;
+ return lf + 2;
+ }
+
+ *separator_len = 0;
+ return msg + strlen(msg);
+}
+
+/*
+ * Build a safe quoted display name from the original 'From' value.
+ * This deliberately uses a conservative fallback for malformed or
+ * control-character-containing values.
+ */
+static void get_display_name(const char *raw_msg, char *out, size_t out_len)
+{
+ if (!out || out_len == 0) return;
+
+ snprintf(out, out_len, "%s", "Subscriber");
+ char *from = get_header_value(raw_msg, "From");
+ if (!from) return;
+
+ char *lt = strchr(from, '<');
+ if (lt) {
+ while (lt > from && isspace((unsigned char)lt[-1])) lt--;
+
+ size_t n = (size_t)(lt - from);
+ while (n && isspace((unsigned char)from[n - 1])) n--;
+
+ /* remove surrounding quotes, if any */
+ const char *start = from;
+ if (n >= 2 && ((from[0] == '"' && from[n - 1] == '"') ||
+ (from[0] == '\'' && from[n - 1] == '\''))) {
+ start++;
+ n -= 2;
+ }
+
+ if (n > 0 && n < out_len) {
+ int safe = 1;
+ for (size_t i = 0; i < n; i++) {
+ unsigned char c = (unsigned char)start[i];
+ if (c < 0x20 || c == 0x7f) {
+ safe = 0;
+ break;
}
}
- } else {
- extract_email_address(raw_from, orig_display, sizeof(orig_display));
+ if (safe) {
+ memcpy(out, start, n);
+ out[n] = '\0';
+ strtrim_whitespace(out);
+ }
}
- 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");
+ /* no display name -- use the parsed address as the display text. */
+ char addr[EMAILLEN_MAX];
+ if (extract_email_address(from, addr, sizeof(addr)) == 0 &&
+ header_value_is_safe(addr))
+ snprintf(out, out_len, "%s", addr);
}
- size_t buf_cap = strlen(raw_msg) + 4096;
- char *out = malloc(buf_cap);
- if (!out) return NULL;
- size_t pos = 0;
+ free(from);
- /* 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
- );
+ if (out[0] == '\0') snprintf(out, out_len, "%s", "Subscriber");
+ return;
+}
- const char *p = raw_msg;
- int stripping_current_header = 0;
+/* append a quoted phrase, escaping quote and backslash */
+static int append_quoted_phrase(message_builder_t *b, const char *s)
+{
+ if (mb_append_cstr(b, "\"") != 0) return -1;
+
+ for (; *s; s++) {
+ if (*s == '"' || *s == '\\') {
+ if (mb_append(b, "\\", 1) != 0) return -1;
+ }
+ if (mb_append(b, s, 1) != 0) return -1;
+ }
+
+ return mb_append_cstr(b, "\"");
+}
+
+/*
+ * Rebuild a message for broadcast.
+ *
+ * Rebuilds raw email buffer: strips stale/auth headers, injects headers
+ * Allocates a buffer in a heap of *out_len length (it sets it too), otherwise nil
+ */
+static char *rebuild_message_for_broadcast(mail_processor_t *mp, const char *raw_msg,
+ size_t *out_len)
+{
+ if (out_len) *out_len = 0;
+
+ if (!mp || !mp->cfg || !raw_msg || !out_len) return NULL;
+
+ const char *list_addr = mp->cfg->maillist_addr;
+ const char *list_name = mp->cfg->maillist_name;
+
+ if (!header_value_is_safe(list_addr) ||
+ !header_value_is_safe(list_name)) return NULL;
+
+ /* Split the configured list address into local part and domain. */
+ const char *at = strchr(list_addr, '@');
+ if (!at || at == list_addr || at[1] == '\0') return NULL;
+
+ size_t local_len = (size_t)(at - list_addr);
+ char *local_part = malloc(local_len + 1);
+ if (!local_part) return NULL;
+
+ memcpy(local_part, list_addr, local_len);
+ local_part[local_len] = '\0';
+ const char *domain_part = at + 1;
+
+ if (!header_value_is_safe(local_part) || !header_value_is_safe(domain_part)) {
+ free(local_part);
+ return NULL;
+ }
+
+ char display[EMAILLEN_MAX];
+ get_display_name(raw_msg, display, sizeof(display));
+
+ message_builder_t out = {0};
+ int failed = 0;
+
+ /* Add list-controlled headers first. */
+ if (mb_append_cstr(&out, "From: ") != 0 ||
+ append_quoted_phrase(&out, display) != 0 ||
+ mb_append_cstr(&out, " via ") != 0 ||
+ append_quoted_phrase(&out, list_name) != 0 ||
+ mb_append_cstr(&out, " <") != 0 ||
+ mb_append_cstr(&out, list_addr) != 0 ||
+ mb_append_cstr(&out, ">\r\nReply-To: <") != 0 ||
+ mb_append_cstr(&out, list_addr) != 0 ||
+ mb_append_cstr(&out, ">\r\nSender: <") != 0 ||
+ mb_append_cstr(&out, list_addr) != 0 ||
+ mb_append_cstr(&out, ">\r\n"
+ "Precedence: list\r\n"
+ "Auto-Submitted: auto-generated\r\n"
+ "List-Id: <") != 0 ||
+ mb_append_cstr(&out, list_addr) != 0 ||
+ mb_append_cstr(&out, ">\r\n"
+ "List-Unsubscribe: \r\nList-Post: \r\n"))
+ failed = 1;
+
+ free(local_part);
+ if (failed) goto error;
+
+ size_t separator_len = 0;
+ const char *body_start = find_body_start(raw_msg, &separator_len);
+ const char *headers_end = body_start - separator_len;
int has_subject = 0;
+ int subject_tagged = 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;
+ /*
+ * Read and copy complete header fields. A field consists of its initial
+ * line plus all following continuation lines. Malformed header lines
+ * cause failure rather than being copied into the rebuilt message.
+ */
+ const char *p = raw_msg;
+ while (p < headers_end) {
+ const char *line_start = p;
+ const char *lf = memchr(p, '\n', (size_t)(headers_end - p));
+ const char *line_end = lf ? lf + 1 : headers_end;
- size_t line_len = line_end - p + 1;
+ /* Empty line before the detected separator: stop header parsing. */
+ const char *content_end = lf ? lf : line_end;
+ if (content_end > line_start && content_end[-1] == '\r') content_end--;
+ if (content_end == line_start) break;
- /* 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;
+ /* Header field names cannot begin with whitespace. */
+ if (*line_start == ' ' || *line_start == '\t') {
+ failed = 1;
+ goto error;
}
- /* Subject Tagging (E003) */
- if (strncasecmp(check_ptr, "Subject:", 8) == 0) {
+ const char *colon = memchr(line_start, ':', (size_t)(content_end - line_start));
+ if (!colon || colon == line_start) {
+ failed = 1;
+ goto error;
+ }
+
+ size_t name_len = (size_t)(colon - line_start);
+ for (size_t i = 0; i < name_len; i++) {
+ unsigned char c = (unsigned char)line_start[i];
+ if (!(isalnum(c) || c == '-')) {
+ failed = 1;
+ goto error;
+ }
+ }
+
+ const char *field_end = line_end;
+
+ /* Consume continuation lines belonging to this field. */
+ while (field_end < headers_end) {
+ const char *next_lf = memchr(field_end, '\n', (size_t)(headers_end - field_end));
+ const char *next_end = next_lf ? next_lf + 1 : headers_end;
+ const char *next_content_end = next_lf ? next_lf : next_end;
+
+ if (next_content_end > field_end && next_content_end[-1] == '\r')
+ next_content_end--;
+ if (next_content_end == field_end || (*field_end != ' ' && *field_end != '\t'))
+ break;
+
+ field_end = next_end;
+ }
+
+ int is_subject = field_name_eq(line_start, name_len, "Subject");
+ int strip = should_strip_header(line_start, name_len);
+
+ if (is_subject) {
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;
+ /* unfold the value only for duplicate tag detection. */
+ message_builder_t unfolded = {0};
+ const char *v = colon + 1;
+ while (v < content_end && (*v == ' ' || *v == '\t')) v++;
+
+ if (mb_append(&unfolded, v, (size_t)(content_end - v)) != 0) {
+ free(unfolded.data);
+ failed = 1;
+ goto error;
+ }
+
+ const char *q = line_end;
+ while (q < field_end) {
+ const char *next_lf = memchr(q, '\n', (size_t)(field_end - q));
+ const char *next_end = next_lf ? next_lf + 1 : field_end;
+ const char *next_content_end = next_lf ? next_lf : next_end;
+
+ if (next_content_end > q && next_content_end[-1] == '\r')
+ next_content_end--;
+
+ while (q < next_content_end && (*q == ' ' || *q == '\t')) q++;
+
+ if (mb_append(&unfolded, " ", 1) != 0 ||
+ mb_append(&unfolded, q, (size_t)(next_content_end - q)) != 0) {
+ free(unfolded.data);
+ failed = 1;
+ goto error;
+ }
+
+ q = next_end;
+ }
+
+ /* IMPROVE: make subject tagging configurable */
+ char tag[TAGLEN_MAX];
+ int n = snprintf(tag, sizeof(tag), "[%s]", list_name);
+ if (n < 0 || (size_t)n >= sizeof(tag)) {
+ free(unfolded.data);
+ failed = 1;
+ goto error;
+ }
+
+ if (unfolded.data && strcasestr(unfolded.data, tag) != NULL)
+ subject_tagged = 1;
+
+ free(unfolded.data);
+ }
+
+ if (!strip) {
+ if (is_subject && !subject_tagged) {
+ if (mb_append_cstr(&out, "Subject: [") != 0 ||
+ mb_append_cstr(&out, list_name) != 0 ||
+ mb_append_cstr(&out, "] ") != 0) {
+ failed = 1;
+ goto error;
+ }
+
+ /* Copy the original subject value and its folded lines. */
+ if (mb_append(&out, colon + 1, (size_t)(field_end - (colon + 1))) != 0) {
+ failed = 1;
+ goto error;
+ }
+
+ /*
+ * The copied field includes its original newline(s). If the
+ * input used LF, keep that style for this field.
+ */
+ if (field_end == headers_end || (field_end > raw_msg && field_end[-1] != '\n')) {
+ if (mb_append_cstr(&out, "\r\n") != 0) {
+ failed = 1;
+ goto error;
+ }
+ }
+ } else {
+ if (mb_append(&out, line_start, (size_t)(field_end - line_start)) != 0) {
+ failed = 1;
+ goto error;
+ }
+
+ if (field_end == headers_end || (field_end > raw_msg && field_end[-1] != '\n')) {
+ if (mb_append_cstr(&out, "\r\n") != 0) {
+ failed = 1;
+ goto error;
+ }
+ }
}
}
- /* 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;
+ p = field_end;
}
if (!has_subject) {
- pos += snprintf(out + pos, buf_cap - pos,
- "Subject: [%s] (No Subject)\r\n", mp->cfg->maillist_name);
+ if (mb_append_cstr(&out, "Subject: [") != 0 ||
+ mb_append_cstr(&out, list_name) != 0 ||
+ mb_append_cstr(&out, "] (No Subject)\r\n") != 0) {
+ failed = 1;
+ goto error;
+ }
}
- /* 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';
+ /* End headers, then append the original body unchanged. */
+ if (mb_append_cstr(&out, "\r\n") != 0 || mb_append(&out, body_start, strlen(body_start)) != 0) {
+ failed = 1;
+ goto error;
}
- *out_len = pos;
- return out;
+ *out_len = out.len;
+ return out.data;
+
+ error:
+ free(out.data);
+ *out_len = 0;
+ return NULL;
}
-int mail_processor_init(mail_processor_t *mp, subrelay_config_t *cfg, subscriber_pool_t *subscribers, pending_pool_t *pending)
+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;
diff --git a/src/misc.c b/src/misc.c
new file mode 100644
index 0000000..47ecfa3
--- /dev/null
+++ b/src/misc.c
@@ -0,0 +1,83 @@
+/*
+ * this is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published
+ * by the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * this is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .";
+ *
+ * (c) Alexander Vdolainen 2007, 2026
+ * subrelay: a minimalistic maillist server based on external IMAPs/SMTPs
+ *
+ * misc.c: misc definitions and functions
+ *
+ */
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+/* Trims whitespace and trailing \r\n */
+char *strtrim_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;
+}
+
+/* MB ops */
+int mb_reserve(message_builder_t *b, size_t additional)
+{
+ if (!b || additional > SIZE_MAX - b->len - 1) return -1;
+
+ size_t needed = b->len + additional + 1;
+ if (needed <= b->cap)
+ return 0;
+
+ size_t cap = b->cap ? b->cap : 1024;
+ while (cap < needed) {
+ if (cap > SIZE_MAX / 2) {
+ cap = needed;
+ break;
+ }
+ cap *= 2;
+ }
+
+ char *tmp = realloc(b->data, cap);
+ if (!tmp) return -1;
+
+ b->data = tmp;
+ b->cap = cap;
+ return 0;
+}
+
+int mb_append(message_builder_t *b, const void *data, size_t len)
+{
+ if ((!data && len) || mb_reserve(b, len) != 0) return -1;
+
+ if (len) memcpy(b->data + b->len, data, len);
+
+ b->len += len;
+ b->data[b->len] = '\0';
+ return 0;
+}
+
+int mb_append_cstr(message_builder_t *b, const char *s)
+{
+ return s ? mb_append(b, s, strlen(s)) : -1;
+}
diff --git a/src/pending.c b/src/pending.c
index 2308ae6..70dbac3 100644
--- a/src/pending.c
+++ b/src/pending.c
@@ -39,18 +39,6 @@ static long pending_cmp(const void *a, const void *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 hex token via /dev/urandom */
static void generate_token(char *buf, size_t len)
{
@@ -188,7 +176,7 @@ int pending_load_file(pending_pool_t *pool)
time_t now = time(NULL);
while (fgets(line, sizeof(line), fp)) {
- char *str = trim_whitespace(line);
+ char *str = strtrim_whitespace(line);
if (*str == '\0' || *str == '#') continue;
/* File line format: TOKEN TIMESTAMP EMAIL */
diff --git a/src/subscribers.c b/src/subscribers.c
index 093cb6d..c5a5f29 100644
--- a/src/subscribers.c
+++ b/src/subscribers.c
@@ -28,6 +28,7 @@
#include
#include
+#include
#include
/* Case-insensitive string comparison for email addresses */
@@ -38,20 +39,6 @@ static long subscriber_cmp(const void *a, const void *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;
@@ -125,7 +112,7 @@ int subscriber_load_file(subscriber_pool_t *pool)
char line[512];
while (fgets(line, sizeof(line), fp)) {
- char *email = trim_whitespace(line);
+ char *email = strtrim_whitespace(line);
/* Ignore empty lines and comment lines starting with '#' */
if (*email == '\0' || *email == '#') continue;