#include <sys_defs.h>
#include <ctype.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#ifdef STRCASECMP_IN_STRINGS_H
#include <strings.h>
#endif
#include <msg.h>
#include <vstring.h>
#include <vstream.h>
#include <argv.h>
#include <split_at.h>
#include <mymalloc.h>
#include <stringops.h>
#include <nvtable.h>
#include <record.h>
#include <rec_type.h>
#include <cleanup_user.h>
#include <tok822.h>
#include <header_opts.h>
#include <quote_822_local.h>
#include <mail_params.h>
#include <mail_date.h>
#include <mail_addr.h>
#include <is_header.h>
#include <ext_prop.h>
#include <mail_proto.h>
#include <mime_state.h>
#include <lex_822.h>
#include "cleanup.h"
static void cleanup_out_header(CLEANUP_STATE *state, VSTRING *header_buf)
{
char *start = vstring_str(header_buf);
char *line;
char *next_line;
for (line = start; line; line = next_line) {
next_line = split_at(line, '\n');
if (line == start || IS_SPACE_TAB(*line)) {
cleanup_out_string(state, REC_TYPE_NORM, line);
} else {
cleanup_out_format(state, REC_TYPE_NORM, "\t%s", line);
}
}
}
static void cleanup_fold_header(CLEANUP_STATE *state, VSTRING *header_buf)
{
char *start_line = vstring_str(header_buf);
char *end_line;
char *next_line;
char *line;
for (line = start_line; line != 0; line = next_line) {
end_line = line + strcspn(line, "\n");
if (line > start_line) {
if (end_line - start_line < 70) {
line[-1] = ' ';
} else {
start_line = line;
}
}
next_line = *end_line ? end_line + 1 : 0;
}
cleanup_out_header(state, header_buf);
}
static char *cleanup_extract_internal(VSTRING *buffer, TOK822 *addr)
{
tok822_internalize(buffer, addr->head, TOK822_STR_DEFL);
return (mystrdup(vstring_str(buffer)));
}
static void cleanup_rewrite_sender(CLEANUP_STATE *state, HEADER_OPTS *hdr_opts,
VSTRING *header_buf)
{
TOK822 *tree;
TOK822 **addr_list;
TOK822 **tpp;
if (msg_verbose)
msg_info("rewrite_sender: %s", hdr_opts->name);
tree = tok822_parse_limit(vstring_str(header_buf)
+ strlen(hdr_opts->name) + 1,
var_token_limit);
addr_list = tok822_grep(tree, TOK822_ADDR);
for (tpp = addr_list; *tpp; tpp++) {
cleanup_rewrite_tree(*tpp);
if (state->flags & CLEANUP_FLAG_MAP_OK) {
if (cleanup_send_canon_maps)
cleanup_map11_tree(state, *tpp, cleanup_send_canon_maps,
cleanup_ext_prop_mask & EXT_PROP_CANONICAL);
if (cleanup_comm_canon_maps)
cleanup_map11_tree(state, *tpp, cleanup_comm_canon_maps,
cleanup_ext_prop_mask & EXT_PROP_CANONICAL);
if (cleanup_masq_domains
&& (cleanup_masq_flags & CLEANUP_MASQ_FLAG_HDR_FROM))
cleanup_masquerade_tree(*tpp, cleanup_masq_domains);
}
if (hdr_opts->type == HDR_FROM && state->from == 0)
state->from = cleanup_extract_internal(header_buf, *tpp);
if (hdr_opts->type == HDR_RESENT_FROM && state->resent_from == 0)
state->resent_from =
cleanup_extract_internal(header_buf, *tpp);
#if 0
if (hdr_opts->type == HDR_RETURN_RECEIPT_TO && !state->return_receipt)
state->return_receipt =
cleanup_extract_internal(header_buf, *tpp);
#endif
if (var_enable_errors_to)
if (hdr_opts->type == HDR_ERRORS_TO && !state->errors_to)
state->errors_to =
cleanup_extract_internal(header_buf, *tpp);
}
vstring_sprintf(header_buf, "%s: ", hdr_opts->name);
tok822_externalize(header_buf, tree, TOK822_STR_HEAD);
myfree((char *) addr_list);
tok822_free_tree(tree);
if ((hdr_opts->flags & HDR_OPT_DROP) == 0)
cleanup_fold_header(state, header_buf);
}
static void cleanup_rewrite_recip(CLEANUP_STATE *state, HEADER_OPTS *hdr_opts,
VSTRING *header_buf)
{
TOK822 *tree;
TOK822 **addr_list;
TOK822 **tpp;
if (msg_verbose)
msg_info("rewrite_recip: %s", hdr_opts->name);
tree = tok822_parse_limit(vstring_str(header_buf)
+ strlen(hdr_opts->name) + 1,
var_token_limit);
addr_list = tok822_grep(tree, TOK822_ADDR);
for (tpp = addr_list; *tpp; tpp++) {
cleanup_rewrite_tree(*tpp);
if (state->flags & CLEANUP_FLAG_MAP_OK) {
if (cleanup_rcpt_canon_maps)
cleanup_map11_tree(state, *tpp, cleanup_rcpt_canon_maps,
cleanup_ext_prop_mask & EXT_PROP_CANONICAL);
if (cleanup_comm_canon_maps)
cleanup_map11_tree(state, *tpp, cleanup_comm_canon_maps,
cleanup_ext_prop_mask & EXT_PROP_CANONICAL);
if (cleanup_masq_domains
&& (cleanup_masq_flags & CLEANUP_MASQ_FLAG_HDR_RCPT))
cleanup_masquerade_tree(*tpp, cleanup_masq_domains);
}
}
vstring_sprintf(header_buf, "%s: ", hdr_opts->name);
tok822_externalize(header_buf, tree, TOK822_STR_HEAD);
myfree((char *) addr_list);
tok822_free_tree(tree);
if ((hdr_opts->flags & HDR_OPT_DROP) == 0)
cleanup_fold_header(state, header_buf);
}
static void cleanup_act_log(CLEANUP_STATE *state,
const char *action, const char *class,
const char *content, const char *text)
{
const char *attr;
if ((attr = nvtable_find(state->attr, MAIL_ATTR_ORIGIN)) == 0)
attr = "unknown";
vstring_sprintf(state->temp1, "%s: %s: %s %.200s from %s;",
state->queue_id, action, class, content, attr);
if (state->sender)
vstring_sprintf_append(state->temp1, " from=<%s>", state->sender);
if (state->recip)
vstring_sprintf_append(state->temp1, " to=<%s>", state->recip);
if ((attr = nvtable_find(state->attr, MAIL_ATTR_PROTO_NAME)) != 0)
vstring_sprintf_append(state->temp1, " proto=%s", attr);
if ((attr = nvtable_find(state->attr, MAIL_ATTR_HELO_NAME)) != 0)
vstring_sprintf_append(state->temp1, " helo=<%s>", attr);
if (text && *text)
vstring_sprintf_append(state->temp1, ": %s", text);
msg_info("%s", vstring_str(state->temp1));
}
#define CLEANUP_ACT_CTXT_HEADER "header"
#define CLEANUP_ACT_CTXT_BODY "body"
static int cleanup_act(CLEANUP_STATE *state, char *context, const char *buf,
const char *value, const char *map_class)
{
const char *optional_text = value + strcspn(value, " \t");
int command_len = optional_text - value;
while (*optional_text && ISSPACE(*optional_text))
optional_text++;
#define STREQUAL(x,y,l) (strncasecmp((x), (y), (l)) == 0 && (y)[l] == 0)
#define CLEANUP_ACT_KEEP 1
#define CLEANUP_ACT_DROP 0
if (STREQUAL(value, "REJECT", command_len)) {
if (state->reason == 0)
state->reason = mystrdup(*optional_text ? optional_text :
cleanup_strerror(CLEANUP_STAT_CONT));
state->errs |= CLEANUP_STAT_CONT;
state->flags &= ~CLEANUP_FLAG_FILTER;
cleanup_act_log(state, "reject", context, buf, state->reason);
return (CLEANUP_ACT_KEEP);
}
if (STREQUAL(value, "WARN", command_len)) {
cleanup_act_log(state, "warning", context, buf, optional_text);
return (CLEANUP_ACT_KEEP);
}
if (STREQUAL(value, "FILTER", command_len)) {
if (*optional_text == 0) {
msg_warn("missing FILTER command argument in %s map", map_class);
} else if (strchr(optional_text, ':') == 0) {
msg_warn("bad FILTER command %s in %s, need transport:destination",
optional_text, map_class);
} else {
if (state->filter)
myfree(state->filter);
state->filter = mystrdup(optional_text);
cleanup_act_log(state, "filter", context, buf, optional_text);
}
return (CLEANUP_ACT_KEEP);
}
if (STREQUAL(value, "DISCARD", command_len)) {
cleanup_act_log(state, "discard", context, buf, optional_text);
state->flags |= CLEANUP_FLAG_DISCARD;
state->flags &= ~CLEANUP_FLAG_FILTER;
return (CLEANUP_ACT_KEEP);
}
if (STREQUAL(value, "HOLD", command_len)) {
cleanup_act_log(state, "hold", context, buf, optional_text);
state->flags |= CLEANUP_FLAG_HOLD;
return (CLEANUP_ACT_KEEP);
}
if (STREQUAL(value, "PREPEND", command_len)) {
if (*optional_text == 0) {
msg_warn("PREPEND action without text in %s map", map_class);
} else if (strcmp(context, CLEANUP_ACT_CTXT_HEADER) == 0
&& !is_header(optional_text)) {
msg_warn("bad PREPEND header text \"%s\" in %s map, "
"need \"headername: headervalue\"",
optional_text, map_class);
} else {
cleanup_act_log(state, "prepend", context, buf, optional_text);
cleanup_out_string(state, REC_TYPE_NORM, optional_text);
}
return (CLEANUP_ACT_KEEP);
}
if (STREQUAL(value, "REDIRECT", command_len)) {
if (strchr(optional_text, '@') == 0) {
msg_warn("bad REDIRECT target \"%s\" in %s map, need user@domain",
optional_text, map_class);
} else {
if (state->redirect)
myfree(state->redirect);
state->redirect = mystrdup(optional_text);
cleanup_act_log(state, "redirect", context, buf, optional_text);
state->flags &= ~CLEANUP_FLAG_FILTER;
}
return (CLEANUP_ACT_KEEP);
}
if (STREQUAL(value, "IGNORE", command_len))
return (CLEANUP_ACT_DROP);
if (STREQUAL(value, "DUNNO", command_len))
return (CLEANUP_ACT_KEEP);
if (STREQUAL(value, "OK", command_len))
return (CLEANUP_ACT_KEEP);
msg_warn("unknown command in %s map: %s", map_class, value);
return (CLEANUP_ACT_KEEP);
}
static void cleanup_header_callback(void *context, int header_class,
HEADER_OPTS *hdr_opts, VSTRING *header_buf,
off_t unused_offset)
{
CLEANUP_STATE *state = (CLEANUP_STATE *) context;
const char *myname = "cleanup_header_callback";
char *hdrval;
struct code_map {
const char *name;
const char *encoding;
};
static struct code_map code_map[] = {
"7bit", MAIL_ATTR_ENC_7BIT,
"8bit", MAIL_ATTR_ENC_8BIT,
"binary", MAIL_ATTR_ENC_8BIT,
"quoted-printable", MAIL_ATTR_ENC_7BIT,
"base64", MAIL_ATTR_ENC_7BIT,
0,
};
struct code_map *cmp;
MAPS *checks;
const char *map_class;
if (msg_verbose)
msg_info("%s: '%.200s'", myname, vstring_str(header_buf));
#define CHECK(class, maps, var_name) \
(header_class == class && (map_class = var_name, checks = maps) != 0)
if (hdr_opts && (hdr_opts->flags & HDR_OPT_MIME))
header_class = MIME_HDR_MULTIPART;
if ((state->flags & CLEANUP_FLAG_FILTER)
&& (CHECK(MIME_HDR_PRIMARY, cleanup_header_checks, VAR_HEADER_CHECKS)
|| CHECK(MIME_HDR_MULTIPART, cleanup_mimehdr_checks, VAR_MIMEHDR_CHECKS)
|| CHECK(MIME_HDR_NESTED, cleanup_nesthdr_checks, VAR_NESTHDR_CHECKS))) {
char *header = vstring_str(header_buf);
const char *value;
if ((value = maps_find(checks, header, 0)) != 0) {
if (cleanup_act(state, CLEANUP_ACT_CTXT_HEADER,
header, value, map_class)
== CLEANUP_ACT_DROP)
return;
}
}
if (hdr_opts == 0) {
cleanup_out_header(state, header_buf);
return;
}
hdrval = vstring_str(header_buf) + strlen(hdr_opts->name) + 1;
while (ISSPACE(*hdrval))
hdrval++;
if (hdr_opts->type == HDR_CONTENT_TRANSFER_ENCODING) {
for (cmp = code_map; cmp->name != 0; cmp++) {
if (strcasecmp(hdrval, cmp->name) == 0) {
if (strcmp(cmp->encoding, MAIL_ATTR_ENC_8BIT) == 0)
nvtable_update(state->attr, MAIL_ATTR_ENCODING,
cmp->encoding);
break;
}
}
}
if (header_class != MIME_HDR_PRIMARY) {
cleanup_out_header(state, header_buf);
return;
}
else {
state->headers_seen |= (1 << hdr_opts->type);
if (hdr_opts->type == HDR_MESSAGE_ID)
msg_info("%s: message-id=%s", state->queue_id, hdrval);
if (hdr_opts->type == HDR_RESENT_MESSAGE_ID)
msg_info("%s: resent-message-id=%s", state->queue_id, hdrval);
if (hdr_opts->type == HDR_RECEIVED)
if (++state->hop_count >= var_hopcount_limit)
state->errs |= CLEANUP_STAT_HOPS;
if (CLEANUP_OUT_OK(state)) {
if (hdr_opts->flags & HDR_OPT_RR)
state->resent = "Resent-";
if (hdr_opts->flags & HDR_OPT_SENDER) {
cleanup_rewrite_sender(state, hdr_opts, header_buf);
} else if (hdr_opts->flags & HDR_OPT_RECIP) {
cleanup_rewrite_recip(state, hdr_opts, header_buf);
} else if ((hdr_opts->flags & HDR_OPT_DROP) == 0) {
cleanup_out_header(state, header_buf);
}
}
}
}
static void cleanup_header_done_callback(void *context)
{
CLEANUP_STATE *state = (CLEANUP_STATE *) context;
char time_stamp[1024];
struct tm *tp;
TOK822 *token;
if ((state->headers_seen & (1 << (state->resent[0] ?
HDR_RESENT_MESSAGE_ID : HDR_MESSAGE_ID))) == 0) {
tp = gmtime(&state->time);
strftime(time_stamp, sizeof(time_stamp), "%Y%m%d%H%M%S", tp);
cleanup_out_format(state, REC_TYPE_NORM, "%sMessage-Id: <%s.%s@%s>",
state->resent, time_stamp, state->queue_id, var_myhostname);
msg_info("%s: %smessage-id=<%s.%s@%s>",
state->queue_id, *state->resent ? "resent-" : "",
time_stamp, state->queue_id, var_myhostname);
}
if ((state->headers_seen & (1 << (state->resent[0] ?
HDR_RESENT_DATE : HDR_DATE))) == 0) {
cleanup_out_format(state, REC_TYPE_NORM, "%sDate: %s",
state->resent, mail_date(state->time));
}
if ((state->headers_seen & (1 << (state->resent[0] ?
HDR_RESENT_FROM : HDR_FROM))) == 0) {
quote_822_local(state->temp1, *state->sender ?
state->sender : MAIL_ADDR_MAIL_DAEMON);
vstring_sprintf(state->temp2, "%sFrom: %s",
state->resent, vstring_str(state->temp1));
if (*state->sender && state->fullname && *state->fullname) {
vstring_sprintf(state->temp1, "(%s)", state->fullname);
token = tok822_parse(vstring_str(state->temp1));
vstring_strcat(state->temp2, " ");
tok822_externalize(state->temp2, token, TOK822_STR_NONE);
tok822_free_tree(token);
}
CLEANUP_OUT_BUF(state, REC_TYPE_NORM, state->temp2);
}
#define VISIBLE_RCPT ((1 << HDR_TO) | (1 << HDR_RESENT_TO) \
| (1 << HDR_CC) | (1 << HDR_RESENT_CC))
if ((state->headers_seen & VISIBLE_RCPT) == 0)
cleanup_out_format(state, REC_TYPE_NORM, "%s", var_rcpt_witheld);
}
static void cleanup_body_callback(void *context, int type,
const char *buf, int len,
off_t offset)
{
CLEANUP_STATE *state = (CLEANUP_STATE *) context;
if ((state->flags & CLEANUP_FLAG_FILTER)
&& cleanup_body_checks
&& (var_body_check_len == 0 || offset < var_body_check_len)) {
const char *value;
if ((value = maps_find(cleanup_body_checks, buf, 0)) != 0) {
if (cleanup_act(state, CLEANUP_ACT_CTXT_BODY,
buf, value, VAR_BODY_CHECKS)
== CLEANUP_ACT_DROP)
return;
}
}
cleanup_out(state, type, buf, len);
}
static void cleanup_message_headerbody(CLEANUP_STATE *state, int type,
const char *buf, int len)
{
char *myname = "cleanup_message_headerbody";
if (type == REC_TYPE_NORM || type == REC_TYPE_CONT) {
state->mime_errs = mime_state_update(state->mime_state, type, buf, len);
}
else if (type == REC_TYPE_XTRA) {
state->mime_errs = mime_state_update(state->mime_state, type, buf, len);
state->mime_errs &= ~MIME_ERR_TRUNC_HEADER;
if (state->mime_errs && state->reason == 0) {
state->errs |= CLEANUP_STAT_CONT;
state->reason = mystrdup(mime_state_error(state->mime_errs));
}
state->mime_state = mime_state_free(state->mime_state);
if ((state->xtra_offset = vstream_ftell(state->dst)) < 0)
msg_fatal("%s: vstream_ftell %s: %m", myname, cleanup_path);
state->action = cleanup_extracted;
}
else {
msg_warn("%s: message rejected: "
"unexpected record type %d in message content", myname, type);
state->errs |= CLEANUP_STAT_BAD;
}
}
static void cleanup_mime_error_callback(void *context, int err_code,
const char *text)
{
CLEANUP_STATE *state = (CLEANUP_STATE *) context;
const char *origin;
if ((err_code & ~MIME_ERR_TRUNC_HEADER) != 0) {
if ((origin = nvtable_find(state->attr, MAIL_ATTR_ORIGIN)) == 0)
origin = MAIL_ATTR_ORG_NONE;
msg_info("%s: reject: mime-error %s: %.100s from %s; from=<%s> to=<%s>",
state->queue_id, mime_state_error(err_code), text, origin,
state->sender, state->recip ? state->recip : "unknown");
}
}
void cleanup_message(CLEANUP_STATE *state, int type, const char *buf, int len)
{
char *myname = "cleanup_message";
int mime_options;
cleanup_out_string(state, REC_TYPE_MESG, "");
if ((state->data_offset = vstream_ftell(state->dst)) < 0)
msg_fatal("%s: vstream_ftell %s: %m", myname, cleanup_path);
mime_options = 0;
if (var_disable_mime_input) {
mime_options |= MIME_OPT_DISABLE_MIME;
} else {
if (state->flags & CLEANUP_FLAG_FILTER) {
if (var_strict_8bitmime || var_strict_7bit_hdrs)
mime_options |= MIME_OPT_REPORT_8BIT_IN_HEADER;
if (var_strict_8bitmime || var_strict_8bit_body)
mime_options |= MIME_OPT_REPORT_8BIT_IN_7BIT_BODY;
if (var_strict_encoding)
mime_options |= MIME_OPT_REPORT_ENCODING_DOMAIN;
if (var_strict_8bitmime || var_strict_7bit_hdrs
|| var_strict_8bit_body || var_strict_encoding
|| *var_header_checks || *var_mimehdr_checks
|| *var_nesthdr_checks)
mime_options |= MIME_OPT_REPORT_NESTING;
}
}
state->mime_state = mime_state_alloc(mime_options,
cleanup_header_callback,
cleanup_header_done_callback,
cleanup_body_callback,
(MIME_STATE_ANY_END) 0,
cleanup_mime_error_callback,
(void *) state);
state->action = cleanup_message_headerbody;
cleanup_message_headerbody(state, type, buf, len);
}