467 lines
14 KiB
C
467 lines
14 KiB
C
|
|
/* 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);
|
||
|
|
}
|