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