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;