/*++ /* NAME /* cleanup_milter 3 /* SUMMARY /* external mail filter support /* SYNOPSIS /* #include /* /* void cleanup_milter_header_checks_init(void) /* /* void cleanup_milter_receive(state, count) /* CLEANUP_STATE *state; /* int count; /* /* void cleanup_milter_inspect(state, milters) /* CLEANUP_STATE *state; /* MILTERS *milters; /* /* cleanup_milter_emul_mail(state, milters, sender) /* CLEANUP_STATE *state; /* MILTERS *milters; /* const char *sender; /* /* cleanup_milter_emul_rcpt(state, milters, recipient) /* CLEANUP_STATE *state; /* MILTERS *milters; /* const char *recipient; /* /* cleanup_milter_emul_data(state, milters) /* CLEANUP_STATE *state; /* MILTERS *milters; /* DESCRIPTION /* This module implements support for Sendmail-style mail /* filter (milter) applications, including in-place queue file /* modification. /* /* cleanup_milter_header_checks_init() does pre-jail /* initializations. /* /* cleanup_milter_receive() receives mail filter definitions, /* typically from an smtpd(8) server process, and registers /* local call-back functions for macro expansion and for queue /* file modification. /* /* cleanup_milter_inspect() sends the current message headers /* and body to the mail filters that were received with /* cleanup_milter_receive(), or that are specified with the /* cleanup_milters configuration parameter. /* /* cleanup_milter_emul_mail() emulates connect, helo and mail /* events for mail that does not arrive via the smtpd(8) server. /* The emulation pretends that mail arrives from localhost/127.0.0.1 /* via ESMTP. Milters can reject emulated connect, helo, mail /* or data events, but not emulated rcpt events as described /* next. /* /* cleanup_milter_emul_rcpt() emulates an rcpt event for mail /* that does not arrive via the smtpd(8) server. This reports /* a server configuration error condition when the milter /* rejects an emulated rcpt event. /* /* cleanup_milter_emul_data() emulates a data event for mail /* that does not arrive via the smtpd(8) server. It's OK for /* milters to reject emulated data events. /* SEE ALSO /* milter(3) generic mail filter interface /* DIAGNOSTICS /* Fatal errors: memory allocation problem. /* Panic: interface violation. /* Warnings: I/O errors (state->errs is updated accordingly). /* LICENSE /* .ad /* .fi /* The Secure Mailer license must be distributed with this software. /* AUTHOR(S) /* Wietse Venema /* IBM T.J. Watson Research /* P.O. Box 704 /* Yorktown Heights, NY 10598, USA /* /* Wietse Venema /* Google, Inc. /* 111 8th Avenue /* New York, NY 10011, USA /*--*/ /* System library. */ #include #include /* AF_INET */ #include #include #ifdef STRCASECMP_IN_STRINGS_H #include #endif /* Utility library. */ #include #include #include #include #include /* Global library. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* Application-specific. */ #include /* * How Postfix 2.4 edits queue file information: * * Mail filter applications (Milters) can send modification requests after * receiving the end of the message body. Postfix implements these * modifications in the cleanup server, so that it can edit the queue file * in place. This avoids the temporary files that would be needed when * modifications were implemented in the SMTP server (Postfix normally does * not store the whole message in main memory). Once a Milter is done * editing, the queue file can be used as input for the next Milter, and so * on. Finally, the cleanup server changes file permissions, calls fsync(), * and waits for successful completion. * * To implement in-place queue file edits, we need to introduce surprisingly * little change to the existing Postfix queue file structure. All we need * is a way to mark a record as deleted, and to jump from one place in the * queue file to another. We could implement deleted records with jumps, but * marking is sometimes simpler. * * Postfix does not store queue files as plain text files. Instead all * information is stored in records with an explicit type and length, for * sender, recipient, arrival time, and so on. Even the content that makes * up the message header and body is stored as records with explicit types * and lengths. This organization makes it very easy to mark a record as * deleted, and to introduce the pointer records that we will use to jump * from one place in a queue file to another place. * * - Deleting a recipient is easiest - simply modify the record type into one * that is skipped by the software that delivers mail. We won't try to reuse * the deleted recipient for other purposes. When deleting a recipient, we * may need to delete multiple recipient records that result from virtual * alias expansion of the original recipient address. * * - Replacing a header record involves pointer records. A record is replaced * by overwriting it with a forward pointer to space after the end of the * queue file, putting the new record there, followed by a reverse pointer * to the record that follows the replaced header. To simplify * implementation we follow a short header record with a filler record so * that we can always overwrite a header record with a pointer. * * N.B. This is a major difference with Postfix version 2.3, which needed * complex code to save records that follow a short header, before it could * overwrite a short header record. This code contained two of the three * post-release bugs that were found with Postfix header editing. * * - Inserting a header record is like replacing one, except that we also * relocate the record that is being overwritten by the forward pointer. * * - Deleting a message header is simplest when we replace it by a "skip" * pointer to the information that follows the header. With a multi-line * header we need to update only the first line. * * - Appending a recipient or header record involves pointer records as well. * To make this convenient, the queue file already contains dummy pointer * records at the locations where we want to append recipient or header * content. To append, change the dummy pointer into a forward pointer to * space after the end of a message, put the new recipient or header record * there, followed by a reverse pointer to the record that follows the * forward pointer. * * - To append another header or recipient record, replace the reverse pointer * by a forward pointer to space after the end of a message, put the new * record there, followed by the value of the reverse pointer that we * replace. Thus, there is no one-to-one correspondence between forward and * backward pointers. Instead, there can be multiple forward pointers for * one reverse pointer. * * - When a mail filter wants to replace an entire body, we overwrite existing * body records until we run out of space, and then write a pointer to space * after the end of the queue file, followed by more body content. There may * be multiple regions with body content; regions are connected by forward * pointers, and the last region ends with a pointer to the marker that ends * the message content segment. Body regions can be large and therefore they * are reused to avoid wasting space. Sendmail mail filters currently do not * replace individual body records, and that is a good thing. * * Making queue file modifications safe: * * Postfix queue files are segmented. The first segment is for envelope * records, the second for message header and body content, and the third * segment is for information that was extracted or generated from the * message header or body content. Each segment is terminated by a marker * record. For now we don't want to change their location. That is, we want * to avoid moving the records that mark the start or end of a queue file * segment. * * To ensure that we can always replace a header or body record by a pointer * record, without having to relocate a marker record, the cleanup server * places a dummy pointer record at the end of the recipients and at the end * of the message header. To support message body modifications, a dummy * pointer record is also placed at the end of the message content. * * With all these changes in queue file organization, REC_TYPE_END is no longer * guaranteed to be the last record in a queue file. If an application were * to read beyond the REC_TYPE_END marker, it would go into an infinite * loop, because records after REC_TYPE_END alternate with reverse pointers * to the middle of the queue file. For robustness, the record reading * routine skips forward to the end-of-file position after reading the * REC_TYPE_END marker. */ /*#define msg_verbose 2*/ static HBC_CHECKS *cleanup_milter_hbc_checks; static VSTRING *cleanup_milter_hbc_reply; static void cleanup_milter_set_error(CLEANUP_STATE *, int); static const char *cleanup_add_rcpt_par(void *, const char *, const char *); #define STR(x) vstring_str(x) #define LEN(x) VSTRING_LEN(x) /* cleanup_milter_hbc_log - log post-milter header/body_checks action */ static void cleanup_milter_hbc_log(void *context, const char *action, const char *where, const char *line, const char *optional_text) { const CLEANUP_STATE *state = (CLEANUP_STATE *) context; const char *attr; vstring_sprintf(state->temp1, "%s: milter-%s-%s: %s %.60s from %s[%s];", state->queue_id, where, action, where, line, state->client_name, state->client_addr); if (state->sender) vstring_sprintf_append(state->temp1, " from=<%s>", info_log_addr_form_sender(state->sender)); if (state->recip) vstring_sprintf_append(state->temp1, " to=<%s>", info_log_addr_form_recipient(state->recip)); if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_PROTO_NAME)) != 0) vstring_sprintf_append(state->temp1, " proto=%s", attr); if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_HELO_NAME)) != 0) vstring_sprintf_append(state->temp1, " helo=<%s>", attr); if (optional_text) vstring_sprintf_append(state->temp1, ": %s", optional_text); msg_info("%s", vstring_str(state->temp1)); } /* cleanup_milter_header_prepend - prepend header to milter-generated header */ static void cleanup_milter_header_prepend(void *context, int rec_type, const char *buf, ssize_t len, off_t offset) { /* XXX save prepended header to buffer. */ msg_warn("the milter_header/body_checks prepend action is not implemented"); } /* cleanup_milter_hbc_extend - additional header/body_checks actions */ static char *cleanup_milter_hbc_extend(void *context, const char *command, ssize_t cmd_len, const char *optional_text, const char *where, const char *buf, ssize_t buf_len, off_t offset) { CLEANUP_STATE *state = (CLEANUP_STATE *) context; const char *map_class = VAR_MILT_HEAD_CHECKS; /* XXX */ #define STREQUAL(x,y,l) (strncasecmp((x), (y), (l)) == 0 && (y)[l] == 0) /* * These are currently our mutually-exclusive ways of not receiving mail: * "reject" and "discard". Only these can be reported to the up-stream * Postfix libmilter code, because sending any reply there causes Postfix * libmilter to skip further "edit" requests. By way of safety net, each * of these must also reset CLEANUP_FLAG_FILTER_ALL. */ #define CLEANUP_MILTER_REJECTING_OR_DISCARDING_MESSAGE(state) \ ((state->flags & CLEANUP_FLAG_DISCARD) || (state->errs & CLEANUP_STAT_CONT)) /* * We log all header/body-checks actions here, because we know the * details of the message content that triggered the action. We report * detail-free milter-reply values (reject/discard, stored in the * milter_hbc_reply state member) to the Postfix libmilter code, so that * Postfix libmilter can stop sending requests. * * We also set all applicable cleanup flags here, because there is no * guarantee that Postfix libmilter will propagate our own milter-reply * value to cleanup_milter_inspect() which calls cleanup_milter_apply(). * The latter translates responses from Milter applications into cleanup * flags, and logs the response text. Postfix libmilter can convey only * one milter-reply value per email message, and that reply may even come * from outside Postfix. * * To suppress redundant logging, cleanup_milter_apply() does nothing when * the milter-reply value matches the saved text in the milter_hbc_reply * state member. As we remember only one milter-reply value, we can't * report multiple milter-reply values per email message. We satisfy this * constraint, because we already clear the CLEANUP_FLAG_FILTER_ALL flags * to terminate further header inspection. */ if ((state->flags & CLEANUP_FLAG_FILTER_ALL) == 0) return ((char *) buf); if (STREQUAL(command, "BCC", cmd_len)) { if (strchr(optional_text, '@') == 0) { msg_warn("bad BCC address \"%s\" in %s map -- " "need user@domain", optional_text, VAR_MILT_HEAD_CHECKS); } else { cleanup_milter_hbc_log(context, "bcc", where, buf, optional_text); /* Caller checks state error flags. */ (void) cleanup_add_rcpt_par(state, optional_text, ""); } return ((char *) buf); } if (STREQUAL(command, "REJECT", cmd_len)) { const CLEANUP_STAT_DETAIL *detail; if (state->reason) myfree(state->reason); detail = cleanup_stat_detail(CLEANUP_STAT_CONT); if (*optional_text) { state->reason = dsn_prepend(detail->dsn, optional_text); if (*state->reason != '4' && *state->reason != '5') { msg_warn("bad DSN action in %s -- need 4.x.x or 5.x.x", optional_text); *state->reason = '4'; } } else { state->reason = dsn_prepend(detail->dsn, detail->text); } if (*state->reason == '4') state->errs |= CLEANUP_STAT_DEFER; else state->errs |= CLEANUP_STAT_CONT; state->flags &= ~CLEANUP_FLAG_FILTER_ALL; cleanup_milter_hbc_log(context, "reject", where, buf, state->reason); vstring_sprintf(cleanup_milter_hbc_reply, "%d %s", detail->smtp, state->reason); STR(cleanup_milter_hbc_reply)[0] = *state->reason; return ((char *) buf); } if (STREQUAL(command, "FILTER", cmd_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_milter_hbc_log(context, "filter", where, buf, optional_text); } return ((char *) buf); } if (STREQUAL(command, "DISCARD", cmd_len)) { cleanup_milter_hbc_log(context, "discard", where, buf, optional_text); vstring_strcpy(cleanup_milter_hbc_reply, "D"); state->flags |= CLEANUP_FLAG_DISCARD; state->flags &= ~CLEANUP_FLAG_FILTER_ALL; return ((char *) buf); } if (STREQUAL(command, "HOLD", cmd_len)) { if ((state->flags & (CLEANUP_FLAG_HOLD | CLEANUP_FLAG_DISCARD)) == 0) { cleanup_milter_hbc_log(context, "hold", where, buf, optional_text); state->flags |= CLEANUP_FLAG_HOLD; } return ((char *) buf); } if (STREQUAL(command, "REDIRECT", cmd_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_milter_hbc_log(context, "redirect", where, buf, optional_text); state->flags &= ~CLEANUP_FLAG_FILTER_ALL; } return ((char *) buf); } return ((char *) HBC_CHECKS_STAT_UNKNOWN); } /* cleanup_milter_header_checks - inspect Milter-generated header */ static int cleanup_milter_header_checks(CLEANUP_STATE *state, VSTRING *buf) { char *ret; /* * Milter application "add/insert/replace header" requests happen at the * end-of-message stage, therefore all the header operations are relative * to the primary message header. */ ret = hbc_header_checks((void *) state, cleanup_milter_hbc_checks, MIME_HDR_PRIMARY, (HEADER_OPTS *) 0, buf, (off_t) 0); if (ret == 0) { return (0); } else if (ret == HBC_CHECKS_STAT_ERROR) { msg_warn("%s: %s map lookup problem -- " "message not accepted, try again later", state->queue_id, VAR_MILT_HEAD_CHECKS); state->errs |= CLEANUP_STAT_WRITE; return (0); } else { if (ret != STR(buf)) { vstring_strcpy(buf, ret); myfree(ret); } return (1); } } /* cleanup_milter_hbc_add_meta_records - add REDIRECT or FILTER meta records */ static void cleanup_milter_hbc_add_meta_records(CLEANUP_STATE *state) { const char *myname = "cleanup_milter_hbc_add_meta_records"; off_t reverse_ptr_offset; off_t new_meta_offset; /* * Note: this code runs while the Milter infrastructure is being torn * down. For this reason we handle all I/O errors here on the spot, * instead of reporting them back through the Milter infrastructure. */ /* * Sanity check. */ if (state->append_meta_pt_offset < 0) msg_panic("%s: no meta append pointer location", myname); if (state->append_meta_pt_target < 0) msg_panic("%s: no meta append pointer target", myname); /* * Allocate space after the end of the queue file, and write the meta * record(s), followed by a reverse pointer record that points to the * target of the old "meta record append" pointer record. This reverse * pointer record becomes the new "meta record append" pointer record. * Although the new "meta record append" pointer record will never be * used, we update it here to make the code more similar to other code * that inserts/appends content, so that common code can be factored out * later. */ if ((new_meta_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) { cleanup_milter_set_error(state, errno); return; } if (state->filter != 0) cleanup_out_string(state, REC_TYPE_FILT, state->filter); if (state->redirect != 0) cleanup_out_string(state, REC_TYPE_RDR, state->redirect); if ((reverse_ptr_offset = vstream_ftell(state->dst)) < 0) { msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path); state->errs |= CLEANUP_STAT_WRITE; return; } cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT, (long) state->append_meta_pt_target); /* * Pointer flipping: update the old "meta record append" pointer record * value with the location of the new meta record. */ if (vstream_fseek(state->dst, state->append_meta_pt_offset, SEEK_SET) < 0) { cleanup_milter_set_error(state, errno); return; } cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT, (long) new_meta_offset); /* * Update the in-memory "meta append" pointer record location with the * location of the reverse pointer record that follows the new meta * record. The target of the "meta append" pointer record does not * change; it's always the record that follows the dummy pointer record * that was written while Postfix received the message. */ state->append_meta_pt_offset = reverse_ptr_offset; /* * Note: state->append_meta_pt_target never changes. */ } /* cleanup_milter_header_checks_init - initialize post-Milter header checks */ void cleanup_milter_header_checks_init(void) { static const char myname[] = "cleanup_milter_header_checks_init"; #define NO_NESTED_HDR_NAME "" #define NO_NESTED_HDR_VALUE "" #define NO_MIME_HDR_NAME "" #define NO_MIME_HDR_VALUE "" static /* XXX not const */ HBC_CALL_BACKS call_backs = { cleanup_milter_hbc_log, cleanup_milter_header_prepend, cleanup_milter_hbc_extend, }; if (*var_milt_head_checks == 0) msg_panic("%s: %s is empty", myname, VAR_MILT_HEAD_CHECKS); if (cleanup_milter_hbc_checks) msg_panic("%s: cleanup_milter_hbc_checks is not null", myname); cleanup_milter_hbc_checks = hbc_header_checks_create(VAR_MILT_HEAD_CHECKS, var_milt_head_checks, NO_MIME_HDR_NAME, NO_MIME_HDR_VALUE, NO_NESTED_HDR_NAME, NO_NESTED_HDR_VALUE, &call_backs); if (cleanup_milter_hbc_reply) msg_panic("%s: cleanup_milter_hbc_reply is not null", myname); cleanup_milter_hbc_reply = vstring_alloc(100); } #ifdef TEST /* cleanup_milter_header_checks_deinit - undo cleanup_milter_header_checks_init */ static void cleanup_milter_header_checks_deinit(void) { static const char myname[] = "cleanup_milter_header_checks_deinit"; if (cleanup_milter_hbc_checks == 0) msg_panic("%s: cleanup_milter_hbc_checks is null", myname); hbc_header_checks_free(cleanup_milter_hbc_checks); cleanup_milter_hbc_checks = 0; if (cleanup_milter_hbc_reply == 0) msg_panic("%s: cleanup_milter_hbc_reply is null", myname); vstring_free(cleanup_milter_hbc_reply); cleanup_milter_hbc_reply = 0; } #endif /* cleanup_milter_header_checks_reinit - re-init post-Milter header checks */ static void cleanup_milter_header_checks_reinit(CLEANUP_STATE *state) { if (state->filter) myfree(state->filter); state->filter = 0; if (state->redirect) myfree(state->redirect); state->redirect = 0; VSTRING_RESET(cleanup_milter_hbc_reply); } /* cleanup_milter_hbc_finish - finalize post-Milter header checks */ static void cleanup_milter_hbc_finish(CLEANUP_STATE *state) { if (CLEANUP_OUT_OK(state) && !CLEANUP_MILTER_REJECTING_OR_DISCARDING_MESSAGE(state) && (state->filter || state->redirect)) cleanup_milter_hbc_add_meta_records(state); } /* * Milter replies. */ #define CLEANUP_MILTER_SET_REASON(__state, __reason) do { \ if ((__state)->reason) \ myfree((__state)->reason); \ (__state)->reason = mystrdup(__reason); \ if ((__state)->smtp_reply) { \ myfree((__state)->smtp_reply); \ (__state)->smtp_reply = 0; \ } \ } while (0) #define CLEANUP_MILTER_SET_SMTP_REPLY(__state, __smtp_reply) do { \ if ((__state)->reason) \ myfree((__state)->reason); \ (__state)->reason = mystrdup(__smtp_reply + 4); \ printable((__state)->reason, '_'); \ if ((__state)->smtp_reply) \ myfree((__state)->smtp_reply); \ (__state)->smtp_reply = mystrdup(__smtp_reply); \ } while (0) /* cleanup_milter_set_error - set error flag from errno */ static void cleanup_milter_set_error(CLEANUP_STATE *state, int err) { if (err == EFBIG) { msg_warn("%s: queue file size limit exceeded", state->queue_id); state->errs |= CLEANUP_STAT_SIZE; } else { msg_warn("%s: write queue file: %m", state->queue_id); state->errs |= CLEANUP_STAT_WRITE; } } /* cleanup_milter_error - return dummy error description */ static const char *cleanup_milter_error(CLEANUP_STATE *state, int err) { const char *myname = "cleanup_milter_error"; const CLEANUP_STAT_DETAIL *dp; /* * For consistency with error reporting within the milter infrastructure, * content manipulation routines return a null pointer on success, and an * SMTP-like response on error. * * However, when cleanup_milter_apply() receives this error response from * the milter infrastructure, it ignores the text since the appropriate * cleanup error flags were already set by cleanup_milter_set_error(). * * Specify a null error number when the "errno to error flag" mapping was * already done elsewhere, possibly outside this module. */ if (err) cleanup_milter_set_error(state, err); else if (CLEANUP_OUT_OK(state)) msg_panic("%s: missing errno to error flag mapping", myname); if (state->milter_err_text == 0) state->milter_err_text = vstring_alloc(50); dp = cleanup_stat_detail(state->errs); return (STR(vstring_sprintf(state->milter_err_text, "%d %s %s", dp->smtp, dp->dsn, dp->text))); } /* cleanup_add_header - append message header */ static const char *cleanup_add_header(void *context, const char *name, const char *space, const char *value) { const char *myname = "cleanup_add_header"; CLEANUP_STATE *state = (CLEANUP_STATE *) context; VSTRING *buf; off_t reverse_ptr_offset; off_t new_hdr_offset; /* * To simplify implementation, the cleanup server writes a dummy "header * append" pointer record after the last message header. We cache both * the location and the target of the current "header append" pointer * record. */ if (state->append_hdr_pt_offset < 0) msg_panic("%s: no header append pointer location", myname); if (state->append_hdr_pt_target < 0) msg_panic("%s: no header append pointer target", myname); /* * Return early when Milter header checks request that this header record * be dropped, or that the message is discarded. Note: CLEANUP_OUT_OK() * tests CLEANUP_FLAG_DISCARD. We don't want to report the latter as an * error. */ buf = vstring_alloc(100); vstring_sprintf(buf, "%s:%s%s", name, space, value); if (cleanup_milter_hbc_checks) { if (cleanup_milter_header_checks(state, buf) == 0 || (state->flags & CLEANUP_FLAG_DISCARD)) { vstring_free(buf); return (0); } if (CLEANUP_OUT_OK(state) == 0) { vstring_free(buf); return (cleanup_milter_error(state, 0)); } } /* * Allocate space after the end of the queue file, and write the header * record(s), followed by a reverse pointer record that points to the * target of the old "header append" pointer record. This reverse pointer * record becomes the new "header append" pointer record. */ if ((new_hdr_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); vstring_free(buf); return (cleanup_milter_error(state, errno)); } /* XXX emit prepended header, then clear it. */ cleanup_out_header(state, buf); /* Includes padding */ vstring_free(buf); if ((reverse_ptr_offset = vstream_ftell(state->dst)) < 0) { msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path); return (cleanup_milter_error(state, errno)); } cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT, (long) state->append_hdr_pt_target); /* * Pointer flipping: update the old "header append" pointer record value * with the location of the new header record. * * XXX To avoid unnecessary seek operations when the new header immediately * follows the old append header pointer, write a null pointer or make * the record reading loop smarter. Making vstream_fseek() smarter does * not help, because it doesn't know if we're going to read or write * after a write+seek sequence. */ if (vstream_fseek(state->dst, state->append_hdr_pt_offset, SEEK_SET) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); return (cleanup_milter_error(state, errno)); } cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT, (long) new_hdr_offset); /* * Update the in-memory "header append" pointer record location with the * location of the reverse pointer record that follows the new header. * The target of the "header append" pointer record does not change; it's * always the record that follows the dummy pointer record that was * written while Postfix received the message. */ state->append_hdr_pt_offset = reverse_ptr_offset; /* * In case of error while doing record output. */ return (CLEANUP_OUT_OK(state) == 0 ? cleanup_milter_error(state, 0) : cleanup_milter_hbc_reply && LEN(cleanup_milter_hbc_reply) ? STR(cleanup_milter_hbc_reply) : 0); /* * Note: state->append_hdr_pt_target never changes. */ } /* hidden_header - respect milter header hiding protocol */ static int hidden_header(VSTRING *buf, ARGV *auto_hdrs, int *hide_done) { char **cpp; int mask; for (cpp = auto_hdrs->argv, mask = 1; *cpp; cpp++, mask <<= 1) if ((*hide_done & mask) == 0 && strncmp(*cpp, STR(buf), LEN(buf)) == 0) return (*hide_done |= mask); return (0); } /* cleanup_find_header_start - find specific header instance */ static off_t cleanup_find_header_start(CLEANUP_STATE *state, ssize_t index, const char *header_label, VSTRING *buf, int *prec_type, int allow_ptr_backup) { const char *myname = "cleanup_find_header_start"; off_t curr_offset; /* offset after found record */ off_t ptr_offset; /* pointer to found record */ VSTRING *ptr_buf = 0; int rec_type = REC_TYPE_ERROR; int last_type; ssize_t len; int hide_done = 0; if (msg_verbose) msg_info("%s: index %ld name \"%s\"", myname, (long) index, header_label ? header_label : "(none)"); /* * Sanity checks. */ if (index < 1) msg_panic("%s: bad header index %ld", myname, (long) index); /* * Skip to the start of the message content, and read records until we * either find the specified header, or until we hit the end of the * headers. * * The index specifies the header instance: 1 is the first one. The header * label specifies the header name. A null pointer matches any header. * * When the specified header is not found, the result value is -1. * * When the specified header is found, its first record is stored in the * caller-provided read buffer, and the result value is the queue file * offset of that record. The file read position is left at the start of * the next (non-filler) queue file record, which can be the remainder of * a multi-record header. * * When a header is found and allow_ptr_backup is non-zero, then the result * is either the first record of that header, or it is the pointer record * that points to the first record of that header. In the latter case, * the file read position is undefined. Returning the pointer allows us * to do some optimizations when inserting text multiple times at the * same place. * * XXX We can't use the MIME processor here. It not only buffers up the * input, it also reads the record that follows a complete header before * it invokes the header call-back action. This complicates the way that * we discover header offsets and boundaries. Worse is that the MIME * processor is unaware that multi-record message headers can have PTR * records in the middle. * * XXX The draw-back of not using the MIME processor is that we have to * duplicate some of its logic here and in the routine that finds the end * of the header record. To minimize the duplication we define an ugly * macro that is used in all code that scans for header boundaries. * * XXX Sendmail compatibility (based on Sendmail 8.13.6 measurements). * * - When changing Received: header #1, we change the Received: header that * follows our own one; a request to change Received: header #0 is * silently treated as a request to change Received: header #1. * * - When changing Date: header #1, we change the first Date: header; a * request to change Date: header #0 is silently treated as a request to * change Date: header #1. * * Thus, header change requests are relative to the content as received, * that is, the content after our own Received: header. They can affect * only the headers that the MTA actually exposes to mail filter * applications. * * - However, when inserting a header at position 0, the new header appears * before our own Received: header, and when inserting at position 1, the * new header appears after our own Received: header. * * Thus, header insert operations are relative to the content as delivered, * that is, the content including our own Received: header. * * None of the above is applicable after a Milter inserts a header before * our own Received: header. From then on, our own Received: header * becomes just like other headers. */ #define CLEANUP_FIND_HEADER_NOTFOUND (-1) #define CLEANUP_FIND_HEADER_IOERROR (-2) #define CLEANUP_FIND_HEADER_RETURN(offs) do { \ if (ptr_buf) \ vstring_free(ptr_buf); \ return (offs); \ } while (0) #define GET_NEXT_TEXT_OR_PTR_RECORD(rec_type, state, buf, curr_offset, quit) \ if ((rec_type = rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE)) < 0) { \ msg_warn("%s: read file %s: %m", myname, cleanup_path); \ cleanup_milter_set_error(state, errno); \ do { quit; } while (0); \ } \ if (msg_verbose > 1) \ msg_info("%s: read: %ld: %.*s", myname, (long) curr_offset, \ LEN(buf) > 30 ? 30 : (int) LEN(buf), STR(buf)); \ if (rec_type == REC_TYPE_DTXT) \ continue; \ if (rec_type != REC_TYPE_NORM && rec_type != REC_TYPE_CONT \ && rec_type != REC_TYPE_PTR) \ break; /* End of hairy macros. */ if (vstream_fseek(state->dst, state->data_offset, SEEK_SET) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); cleanup_milter_set_error(state, errno); CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR); } for (ptr_offset = 0, last_type = 0; /* void */ ; /* void */ ) { if ((curr_offset = vstream_ftell(state->dst)) < 0) { msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path); cleanup_milter_set_error(state, errno); CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR); } /* Don't follow the "append header" pointer. */ if (curr_offset == state->append_hdr_pt_offset) break; /* Caution: this macro terminates the loop at end-of-message. */ /* Don't do complex processing while breaking out of this loop. */ GET_NEXT_TEXT_OR_PTR_RECORD(rec_type, state, buf, curr_offset, CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR)); /* Caution: don't assume ptr->header. This may be header-ptr->body. */ if (rec_type == REC_TYPE_PTR) { if (rec_goto(state->dst, STR(buf)) < 0) { msg_warn("%s: read file %s: %m", myname, cleanup_path); cleanup_milter_set_error(state, errno); CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR); } /* Save PTR record, in case it points to the start of a header. */ if (allow_ptr_backup) { ptr_offset = curr_offset; if (ptr_buf == 0) ptr_buf = vstring_alloc(100); vstring_strcpy(ptr_buf, STR(buf)); } /* Don't update last_type; PTR can happen after REC_TYPE_CONT. */ continue; } /* The middle of a multi-record header. */ else if (last_type == REC_TYPE_CONT || IS_SPACE_TAB(STR(buf)[0])) { /* Reset the saved PTR record and update last_type. */ } /* No more message headers. */ else if ((len = is_header(STR(buf))) == 0) { break; } /* This the start of a message header. */ else if ((header_label == 0 || (strncasecmp(header_label, STR(buf), len) == 0 && strlen(header_label) == len && !hidden_header(buf, state->auto_hdrs, &hide_done))) && --index == 0) { /* If we have a saved PTR record, it points to start of header. */ break; } ptr_offset = 0; last_type = rec_type; } /* * In case of failure, return negative start position. */ if (index > 0) { curr_offset = CLEANUP_FIND_HEADER_NOTFOUND; } else { /* * Skip over short-header padding, so that the file read pointer is * always positioned at the first non-padding record after the header * record. Insist on padding after short a header record, so that a * short header record can safely be overwritten by a pointer record. */ if (LEN(buf) < REC_TYPE_PTR_PAYL_SIZE) { VSTRING *rbuf = (ptr_offset ? buf : (ptr_buf ? ptr_buf : (ptr_buf = vstring_alloc(100)))); int rval; if ((rval = rec_get_raw(state->dst, rbuf, 0, REC_FLAG_NONE)) < 0) { cleanup_milter_set_error(state, errno); CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR); } if (rval != REC_TYPE_DTXT) msg_panic("%s: short header without padding", myname); } /* * Optionally return a pointer to the message header, instead of the * start of the message header itself. In that case the file read * position is undefined (actually it is at the first non-padding * record that follows the message header record). */ if (ptr_offset != 0) { rec_type = REC_TYPE_PTR; curr_offset = ptr_offset; vstring_strcpy(buf, STR(ptr_buf)); } *prec_type = rec_type; } if (msg_verbose) msg_info("%s: index %ld name %s type %d offset %ld", myname, (long) index, header_label ? header_label : "(none)", rec_type, (long) curr_offset); CLEANUP_FIND_HEADER_RETURN(curr_offset); } /* cleanup_find_header_end - find end of header */ static off_t cleanup_find_header_end(CLEANUP_STATE *state, VSTRING *rec_buf, int last_type) { const char *myname = "cleanup_find_header_end"; off_t read_offset; int rec_type; /* * This routine is called immediately after cleanup_find_header_start(). * rec_buf is the cleanup_find_header_start() result record; last_type is * the corresponding record type: REC_TYPE_PTR or REC_TYPE_NORM; the file * read position is at the first non-padding record after the result * header record. */ for (;;) { if ((read_offset = vstream_ftell(state->dst)) < 0) { msg_warn("%s: read file %s: %m", myname, cleanup_path); cleanup_milter_error(state, errno); return (-1); } /* Don't follow the "append header" pointer. */ if (read_offset == state->append_hdr_pt_offset) break; /* Caution: this macro terminates the loop at end-of-message. */ /* Don't do complex processing while breaking out of this loop. */ GET_NEXT_TEXT_OR_PTR_RECORD(rec_type, state, rec_buf, read_offset, /* Warning and errno->error mapping are done elsewhere. */ return (-1)); if (rec_type == REC_TYPE_PTR) { if (rec_goto(state->dst, STR(rec_buf)) < 0) { msg_warn("%s: read file %s: %m", myname, cleanup_path); cleanup_milter_error(state, errno); return (-1); } /* Don't update last_type; PTR may follow REC_TYPE_CONT. */ continue; } /* Start of header or message body. */ if (last_type != REC_TYPE_CONT && !IS_SPACE_TAB(STR(rec_buf)[0])) break; last_type = rec_type; } return (read_offset); } /* cleanup_patch_header - patch new header into an existing header */ static const char *cleanup_patch_header(CLEANUP_STATE *state, const char *new_hdr_name, const char *hdr_space, const char *new_hdr_value, off_t old_rec_offset, int old_rec_type, VSTRING *old_rec_buf, off_t next_offset) { const char *myname = "cleanup_patch_header"; VSTRING *buf = vstring_alloc(100); off_t new_hdr_offset; #define CLEANUP_PATCH_HEADER_RETURN(ret) do { \ vstring_free(buf); \ return (ret); \ } while (0) if (msg_verbose) msg_info("%s: \"%s\" \"%s\" at %ld", myname, new_hdr_name, new_hdr_value, (long) old_rec_offset); /* * Allocate space after the end of the queue file for the new header and * optionally save an existing record to make room for a forward pointer * record. If the saved record was not a PTR record, follow the saved * record by a reverse pointer record that points to the record after the * original location of the saved record. * * We update the queue file in a safe manner: save the new header and the * existing records after the end of the queue file, write the reverse * pointer, and only then overwrite the saved records with the forward * pointer to the new header. * * old_rec_offset, old_rec_type, and old_rec_buf specify the record that we * are about to overwrite with a pointer record. If the record needs to * be saved (i.e. old_rec_type > 0), the buffer contains the data content * of exactly one PTR or text record. * * next_offset specifies the record that follows the to-be-overwritten * record. It is ignored when the to-be-saved record is a pointer record. */ /* * Return early when Milter header checks request that this header record * be dropped. */ vstring_sprintf(buf, "%s:%s%s", new_hdr_name, hdr_space, new_hdr_value); if (cleanup_milter_hbc_checks && cleanup_milter_header_checks(state, buf) == 0) CLEANUP_PATCH_HEADER_RETURN(0); /* * Write the new header to a new location after the end of the queue * file. */ if ((new_hdr_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); CLEANUP_PATCH_HEADER_RETURN(cleanup_milter_error(state, errno)); } /* XXX emit prepended header, then clear it. */ cleanup_out_header(state, buf); /* Includes padding */ if (msg_verbose > 1) msg_info("%s: %ld: write %.*s", myname, (long) new_hdr_offset, LEN(buf) > 30 ? 30 : (int) LEN(buf), STR(buf)); /* * Optionally, save the existing text record or pointer record that will * be overwritten with the forward pointer. Pad a short saved record to * ensure that it, too, can be overwritten by a pointer. */ if (old_rec_type > 0) { CLEANUP_OUT_BUF(state, old_rec_type, old_rec_buf); if (LEN(old_rec_buf) < REC_TYPE_PTR_PAYL_SIZE) rec_pad(state->dst, REC_TYPE_DTXT, REC_TYPE_PTR_PAYL_SIZE - LEN(old_rec_buf)); if (msg_verbose > 1) msg_info("%s: write %.*s", myname, LEN(old_rec_buf) > 30 ? 30 : (int) LEN(old_rec_buf), STR(old_rec_buf)); } /* * If the saved record wasn't a PTR record, write the reverse pointer * after the saved records. A reverse pointer value of -1 means we were * confused about what we were going to save. */ if (old_rec_type != REC_TYPE_PTR) { if (next_offset < 0) msg_panic("%s: bad reverse pointer %ld", myname, (long) next_offset); cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT, (long) next_offset); if (msg_verbose > 1) msg_info("%s: write PTR %ld", myname, (long) next_offset); } /* * Write the forward pointer over the old record. Generally, a pointer * record will be shorter than a header record, so there will be a gap in * the queue file before the next record. In other words, we must always * follow pointer records otherwise we get out of sync with the data. */ if (vstream_fseek(state->dst, old_rec_offset, SEEK_SET) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); CLEANUP_PATCH_HEADER_RETURN(cleanup_milter_error(state, errno)); } cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT, (long) new_hdr_offset); if (msg_verbose > 1) msg_info("%s: %ld: write PTR %ld", myname, (long) old_rec_offset, (long) new_hdr_offset); /* * In case of error while doing record output. */ CLEANUP_PATCH_HEADER_RETURN( CLEANUP_OUT_OK(state) == 0 ? cleanup_milter_error(state, 0) : cleanup_milter_hbc_reply && LEN(cleanup_milter_hbc_reply) ? STR(cleanup_milter_hbc_reply) : 0); /* * Note: state->append_hdr_pt_target never changes. */ } /* cleanup_ins_header - insert message header */ static const char *cleanup_ins_header(void *context, ssize_t index, const char *new_hdr_name, const char *hdr_space, const char *new_hdr_value) { const char *myname = "cleanup_ins_header"; CLEANUP_STATE *state = (CLEANUP_STATE *) context; VSTRING *old_rec_buf = vstring_alloc(100); off_t old_rec_offset; int old_rec_type; off_t next_offset; const char *ret; #define CLEANUP_INS_HEADER_RETURN(ret) do { \ vstring_free(old_rec_buf); \ return (ret); \ } while (0) if (msg_verbose) msg_info("%s: %ld \"%s\" \"%s\"", myname, (long) index, new_hdr_name, new_hdr_value); /* * Look for a header at the specified position. * * The lookup result may be a pointer record. This allows us to make some * optimization when multiple insert operations happen in the same place. * * Index 1 is the top-most header. */ #define NO_HEADER_NAME ((char *) 0) #define ALLOW_PTR_BACKUP 1 if (index < 1) index = 1; old_rec_offset = cleanup_find_header_start(state, index, NO_HEADER_NAME, old_rec_buf, &old_rec_type, ALLOW_PTR_BACKUP); if (old_rec_offset == CLEANUP_FIND_HEADER_IOERROR) /* Warning and errno->error mapping are done elsewhere. */ CLEANUP_INS_HEADER_RETURN(cleanup_milter_error(state, 0)); /* * If the header does not exist, simply append the header to the linked * list at the "header append" pointer record. */ if (old_rec_offset < 0) CLEANUP_INS_HEADER_RETURN(cleanup_add_header(context, new_hdr_name, hdr_space, new_hdr_value)); /* * If the header does exist, save both the new and the existing header to * new storage at the end of the queue file, and link the new storage * with a forward and reverse pointer (don't write a reverse pointer if * we are starting with a pointer record). */ if (old_rec_type == REC_TYPE_PTR) { next_offset = -1; } else { if ((next_offset = vstream_ftell(state->dst)) < 0) { msg_warn("%s: read file %s: %m", myname, cleanup_path); CLEANUP_INS_HEADER_RETURN(cleanup_milter_error(state, errno)); } } ret = cleanup_patch_header(state, new_hdr_name, hdr_space, new_hdr_value, old_rec_offset, old_rec_type, old_rec_buf, next_offset); CLEANUP_INS_HEADER_RETURN(ret); } /* cleanup_upd_header - modify or append message header */ static const char *cleanup_upd_header(void *context, ssize_t index, const char *new_hdr_name, const char *hdr_space, const char *new_hdr_value) { const char *myname = "cleanup_upd_header"; CLEANUP_STATE *state = (CLEANUP_STATE *) context; VSTRING *rec_buf; off_t old_rec_offset; off_t next_offset; int last_type; const char *ret; if (msg_verbose) msg_info("%s: %ld \"%s\" \"%s\"", myname, (long) index, new_hdr_name, new_hdr_value); /* * Sanity check. */ if (*new_hdr_name == 0) msg_panic("%s: null header name", myname); /* * Find the header that is being modified. * * The lookup result will never be a pointer record. * * Index 1 is the first matching header instance. * * XXX When a header is updated repeatedly we create jumps to jumps. To * eliminate this, rewrite the loop below so that we can start with the * pointer record that points to the header that's being edited. */ #define DONT_SAVE_RECORD 0 #define NO_PTR_BACKUP 0 #define CLEANUP_UPD_HEADER_RETURN(ret) do { \ vstring_free(rec_buf); \ return (ret); \ } while (0) rec_buf = vstring_alloc(100); old_rec_offset = cleanup_find_header_start(state, index, new_hdr_name, rec_buf, &last_type, NO_PTR_BACKUP); if (old_rec_offset == CLEANUP_FIND_HEADER_IOERROR) /* Warning and errno->error mapping are done elsewhere. */ CLEANUP_UPD_HEADER_RETURN(cleanup_milter_error(state, 0)); /* * If no old header is found, simply append the new header to the linked * list at the "header append" pointer record. */ if (old_rec_offset < 0) CLEANUP_UPD_HEADER_RETURN(cleanup_add_header(context, new_hdr_name, hdr_space, new_hdr_value)); /* * If the old header is found, find the end of the old header, save the * new header to new storage at the end of the queue file, and link the * new storage with a forward and reverse pointer. */ if ((next_offset = cleanup_find_header_end(state, rec_buf, last_type)) < 0) /* Warning and errno->error mapping are done elsewhere. */ CLEANUP_UPD_HEADER_RETURN(cleanup_milter_error(state, 0)); ret = cleanup_patch_header(state, new_hdr_name, hdr_space, new_hdr_value, old_rec_offset, DONT_SAVE_RECORD, (VSTRING *) 0, next_offset); CLEANUP_UPD_HEADER_RETURN(ret); } /* cleanup_del_header - delete message header */ static const char *cleanup_del_header(void *context, ssize_t index, const char *hdr_name) { const char *myname = "cleanup_del_header"; CLEANUP_STATE *state = (CLEANUP_STATE *) context; VSTRING *rec_buf; off_t header_offset; off_t next_offset; int last_type; if (msg_verbose) msg_info("%s: %ld \"%s\"", myname, (long) index, hdr_name); /* * Sanity check. */ if (*hdr_name == 0) msg_panic("%s: null header name", myname); /* * Find the header that is being deleted. * * The lookup result will never be a pointer record. * * Index 1 is the first matching header instance. */ #define CLEANUP_DEL_HEADER_RETURN(ret) do { \ vstring_free(rec_buf); \ return (ret); \ } while (0) rec_buf = vstring_alloc(100); header_offset = cleanup_find_header_start(state, index, hdr_name, rec_buf, &last_type, NO_PTR_BACKUP); if (header_offset == CLEANUP_FIND_HEADER_IOERROR) /* Warning and errno->error mapping are done elsewhere. */ CLEANUP_DEL_HEADER_RETURN(cleanup_milter_error(state, 0)); /* * Overwrite the beginning of the header record with a pointer to the * information that follows the header. We can't simply overwrite the * header with cleanup_out_header() and a special record type, because * there may be a PTR record in the middle of a multi-line header. */ if (header_offset > 0) { if ((next_offset = cleanup_find_header_end(state, rec_buf, last_type)) < 0) /* Warning and errno->error mapping are done elsewhere. */ CLEANUP_DEL_HEADER_RETURN(cleanup_milter_error(state, 0)); /* Mark the header as deleted. */ if (vstream_fseek(state->dst, header_offset, SEEK_SET) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); CLEANUP_DEL_HEADER_RETURN(cleanup_milter_error(state, errno)); } rec_fprintf(state->dst, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT, (long) next_offset); } vstring_free(rec_buf); /* * In case of error while doing record output. */ return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0)); } /* cleanup_chg_from - replace sender address, ignore ESMTP arguments */ static const char *cleanup_chg_from(void *context, const char *ext_from, const char *esmtp_args) { const char *myname = "cleanup_chg_from"; CLEANUP_STATE *state = (CLEANUP_STATE *) context; off_t new_offset; off_t new_sender_offset; off_t after_sender_offs; int addr_count; TOK822 *tree; TOK822 *tp; VSTRING *int_sender_buf; int dsn_envid = 0; int dsn_ret = 0; if (msg_verbose) msg_info("%s: \"%s\" \"%s\"", myname, ext_from, esmtp_args); /* * ESMTP support is limited to RET and ENVID, i.e. things that are stored * together with the sender queue file record. */ if (esmtp_args[0]) { ARGV *esmtp_argv; int i; const char *arg; esmtp_argv = argv_split(esmtp_args, " "); for (i = 0; i < esmtp_argv->argc; ++i) { arg = esmtp_argv->argv[i]; if (strncasecmp(arg, "RET=", 4) == 0) { if ((dsn_ret = dsn_ret_code(arg + 4)) == 0) { msg_warn("Ignoring bad ESMTP parameter \"%s\" in " "SMFI_CHGFROM request", arg); } else { state->dsn_ret = dsn_ret; } } else if (strncasecmp(arg, "ENVID=", 6) == 0) { if (state->milter_dsn_buf == 0) state->milter_dsn_buf = vstring_alloc(20); dsn_envid = (xtext_unquote(state->milter_dsn_buf, arg + 6) && allprint(STR(state->milter_dsn_buf))); if (!dsn_envid) { msg_warn("Ignoring bad ESMTP parameter \"%s\" in " "SMFI_CHGFROM request", arg); } else { if (state->dsn_envid) myfree(state->dsn_envid); state->dsn_envid = mystrdup(STR(state->milter_dsn_buf)); } } else { msg_warn("Ignoring bad ESMTP parameter \"%s\" in " "SMFI_CHGFROM request", arg); } } argv_free(esmtp_argv); } /* * The cleanup server remembers the file offset of the current sender * address record (offset in sender_pt_offset) and the file offset of the * record that follows the sender address (offset in sender_pt_target). * Short original sender records are padded, so that they can safely be * overwritten with a pointer record to the new sender address record. */ if (state->sender_pt_offset < 0) msg_panic("%s: no original sender record offset", myname); if (state->sender_pt_target < 0) msg_panic("%s: no post-sender record offset", myname); /* * Allocate space after the end of the queue file, and write the new {DSN * envid, DSN ret, sender address, sender BCC} records, followed by a * reverse pointer record that points to the record that follows the * original sender record. * * We update the queue file in a safe manner: save the new sender after the * end of the queue file, write the reverse pointer, and only then * overwrite the old sender record with the forward pointer to the new * sender. */ if ((new_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); return (cleanup_milter_error(state, errno)); } /* * Sender DSN attribute records precede the sender record. */ if (dsn_envid) rec_fprintf(state->dst, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_DSN_ENVID, STR(state->milter_dsn_buf)); if (dsn_ret) rec_fprintf(state->dst, REC_TYPE_ATTR, "%s=%d", MAIL_ATTR_DSN_RET, dsn_ret); if (dsn_envid == 0 && dsn_ret == 0) { new_sender_offset = new_offset; } else if ((new_sender_offset = vstream_ftell(state->dst)) < 0) { msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path); return (cleanup_milter_error(state, errno)); } /* * Transform the address from external form to internal form. This also * removes the enclosing <>, if present. * * XXX vstring_alloc() rejects zero-length requests. */ int_sender_buf = vstring_alloc(strlen(ext_from) + 1); tree = tok822_parse(ext_from); for (addr_count = 0, tp = tree; tp != 0; tp = tp->next) { if (tp->type == TOK822_ADDR) { if (addr_count == 0) { tok822_internalize(int_sender_buf, tp->head, TOK822_STR_DEFL); addr_count += 1; } else { msg_warn("%s: Milter request to add multi-sender: \"%s\"", state->queue_id, ext_from); break; } } } tok822_free_tree(tree); after_sender_offs = cleanup_addr_sender(state, STR(int_sender_buf)); vstring_free(int_sender_buf); cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT, (long) state->sender_pt_target); state->sender_pt_target = after_sender_offs; /* * Overwrite the current sender record with the pointer to the new {DSN * envid, DSN ret, sender address, sender BCC} records. */ if (vstream_fseek(state->dst, state->sender_pt_offset, SEEK_SET) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); return (cleanup_milter_error(state, errno)); } cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT, (long) new_offset); /* * Remember the location of the new current sender record. */ state->sender_pt_offset = new_sender_offset; /* * In case of error while doing record output. */ return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0)); } /* cleanup_add_rcpt_par - append recipient address, with ESMTP arguments */ static const char *cleanup_add_rcpt_par(void *context, const char *ext_rcpt, const char *esmtp_args) { const char *myname = "cleanup_add_rcpt_par"; CLEANUP_STATE *state = (CLEANUP_STATE *) context; off_t new_rcpt_offset; off_t reverse_ptr_offset; int addr_count; TOK822 *tree; TOK822 *tp; VSTRING *int_rcpt_buf; VSTRING *orcpt_buf = 0; ARGV *esmtp_argv; int dsn_notify = 0; const char *dsn_orcpt_info = 0; size_t type_len; int i; const char *arg; const char *arg_val; if (msg_verbose) msg_info("%s: \"%s\" \"%s\"", myname, ext_rcpt, esmtp_args); /* * To simplify implementation, the cleanup server writes a dummy * "recipient append" pointer record after the last recipient. We cache * both the location and the target of the current "recipient append" * pointer record. */ if (state->append_rcpt_pt_offset < 0) msg_panic("%s: no recipient append pointer location", myname); if (state->append_rcpt_pt_target < 0) msg_panic("%s: no recipient append pointer target", myname); /* * Allocate space after the end of the queue file, and write the * recipient record, followed by a reverse pointer record that points to * the target of the old "recipient append" pointer record. This reverse * pointer record becomes the new "recipient append" pointer record. * * We update the queue file in a safe manner: save the new recipient after * the end of the queue file, write the reverse pointer, and only then * overwrite the old "recipient append" pointer with the forward pointer * to the new recipient. */ if ((new_rcpt_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); return (cleanup_milter_error(state, errno)); } /* * Parse ESMTP parameters. XXX UTF8SMTP don't assume ORCPT is xtext. */ if (esmtp_args[0]) { esmtp_argv = argv_split(esmtp_args, " "); for (i = 0; i < esmtp_argv->argc; ++i) { arg = esmtp_argv->argv[i]; if (strncasecmp(arg, "NOTIFY=", 7) == 0) { /* RFC 3461 */ if (dsn_notify || (dsn_notify = dsn_notify_mask(arg + 7)) == 0) msg_warn("%s: Bad NOTIFY parameter from Milter or " "header/body_checks: \"%.100s\"", state->queue_id, arg); } else if (strncasecmp(arg, "ORCPT=", 6) == 0) { /* RFC 3461 */ if (orcpt_buf == 0) orcpt_buf = vstring_alloc(100); if (dsn_orcpt_info || (type_len = strcspn(arg_val = arg + 6, ";")) == 0 || (arg_val)[type_len] != ';' || xtext_unquote_append(vstring_sprintf(orcpt_buf, "%.*s;", (int) type_len, arg_val), arg_val + type_len + 1) == 0) { msg_warn("%s: Bad ORCPT parameter from Milter or " "header/body_checks: \"%.100s\"", state->queue_id, arg); } else { dsn_orcpt_info = STR(orcpt_buf); } } else { msg_warn("%s: ignoring ESMTP argument from Milter or " "header/body_checks: \"%.100s\"", state->queue_id, arg); } } argv_free(esmtp_argv); } /* * Transform recipient from external form to internal form. This also * removes the enclosing <>, if present. * * XXX vstring_alloc() rejects zero-length requests. */ int_rcpt_buf = vstring_alloc(strlen(ext_rcpt) + 1); tree = tok822_parse(ext_rcpt); for (addr_count = 0, tp = tree; tp != 0; tp = tp->next) { if (tp->type == TOK822_ADDR) { if (addr_count == 0) { tok822_internalize(int_rcpt_buf, tp->head, TOK822_STR_DEFL); addr_count += 1; } else { msg_warn("%s: Milter or header/body_checks request to " "add multi-recipient: \"%s\"", state->queue_id, ext_rcpt); break; } } } tok822_free_tree(tree); if (addr_count != 0) cleanup_addr_bcc_dsn(state, STR(int_rcpt_buf), dsn_orcpt_info, dsn_notify ? dsn_notify : DEF_DSN_NOTIFY); else msg_warn("%s: ignoring attempt from Milter to add null recipient", state->queue_id); vstring_free(int_rcpt_buf); if (orcpt_buf) vstring_free(orcpt_buf); /* * Don't update the queue file when we did not write a recipient record * (malformed or duplicate BCC recipient). */ if (vstream_ftell(state->dst) == new_rcpt_offset) return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0)); /* * Follow the recipient with a "reverse" pointer to the old recipient * append target. */ if ((reverse_ptr_offset = vstream_ftell(state->dst)) < 0) { msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path); return (cleanup_milter_error(state, errno)); } cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT, (long) state->append_rcpt_pt_target); /* * Pointer flipping: update the old "recipient append" pointer record * value to the location of the new recipient record. */ if (vstream_fseek(state->dst, state->append_rcpt_pt_offset, SEEK_SET) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); return (cleanup_milter_error(state, errno)); } cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT, (long) new_rcpt_offset); /* * Update the in-memory "recipient append" pointer record location with * the location of the reverse pointer record that follows the new * recipient. The target of the "recipient append" pointer record does * not change; it's always the record that follows the dummy pointer * record that was written while Postfix received the message. */ state->append_rcpt_pt_offset = reverse_ptr_offset; /* * In case of error while doing record output. */ return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0)); } /* cleanup_add_rcpt - append recipient address */ static const char *cleanup_add_rcpt(void *context, const char *ext_rcpt) { return (cleanup_add_rcpt_par(context, ext_rcpt, "")); } /* cleanup_del_rcpt - remove recipient and all its expansions */ static const char *cleanup_del_rcpt(void *context, const char *ext_rcpt) { const char *myname = "cleanup_del_rcpt"; CLEANUP_STATE *state = (CLEANUP_STATE *) context; off_t curr_offset; VSTRING *buf; char *attr_name; char *attr_value; char *dsn_orcpt = 0; /* XXX for dup filter cleanup */ int dsn_notify = 0; /* XXX for dup filter cleanup */ char *orig_rcpt = 0; char *start; int rec_type; int junk; int count = 0; TOK822 *tree; TOK822 *tp; VSTRING *int_rcpt_buf; int addr_count; if (msg_verbose) msg_info("%s: \"%s\"", myname, ext_rcpt); /* * Virtual aliasing and other address rewriting happens after the mail * filter sees the envelope address. Therefore we must delete all * recipient records whose Postfix (not DSN) original recipient address * matches the specified address. * * As the number of recipients may be very large we can't do an efficient * two-pass implementation (collect record offsets first, then mark * records as deleted). Instead we mark records as soon as we find them. * This is less efficient because we do (seek-write-read) for each marked * recipient, instead of (seek-write). It's unlikely that VSTREAMs will * be made smart enough to eliminate unnecessary I/O with small seeks. * * XXX When Postfix original recipients are turned off, we have no option * but to match against the expanded and rewritten recipient address. * * XXX Remove the (dsn_orcpt, dsn_notify, orcpt, recip) tuple from the * duplicate recipient filter. This requires that we maintain reference * counts. */ if (vstream_fseek(state->dst, 0L, SEEK_SET) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); return (cleanup_milter_error(state, errno)); } #define CLEANUP_DEL_RCPT_RETURN(ret) do { \ if (orig_rcpt != 0) \ myfree(orig_rcpt); \ if (dsn_orcpt != 0) \ myfree(dsn_orcpt); \ vstring_free(buf); \ vstring_free(int_rcpt_buf); \ return (ret); \ } while (0) /* * Transform recipient from external form to internal form. This also * removes the enclosing <>, if present. * * XXX vstring_alloc() rejects zero-length requests. */ int_rcpt_buf = vstring_alloc(strlen(ext_rcpt) + 1); tree = tok822_parse(ext_rcpt); for (addr_count = 0, tp = tree; tp != 0; tp = tp->next) { if (tp->type == TOK822_ADDR) { if (addr_count == 0) { tok822_internalize(int_rcpt_buf, tp->head, TOK822_STR_DEFL); addr_count += 1; } else { msg_warn("%s: Milter request to drop multi-recipient: \"%s\"", state->queue_id, ext_rcpt); break; } } } tok822_free_tree(tree); buf = vstring_alloc(100); for (;;) { if (CLEANUP_OUT_OK(state) == 0) /* Warning and errno->error mapping are done elsewhere. */ CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, 0)); if ((curr_offset = vstream_ftell(state->dst)) < 0) { msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path); CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno)); } if ((rec_type = rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE)) <= 0) { msg_warn("%s: read file %s: %m", myname, cleanup_path); CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno)); } if (rec_type == REC_TYPE_END) break; /* Skip over message content. */ if (rec_type == REC_TYPE_MESG) { if (vstream_fseek(state->dst, state->xtra_offset, SEEK_SET) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno)); } continue; } start = STR(buf); if (rec_type == REC_TYPE_PTR) { if (rec_goto(state->dst, start) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno)); } continue; } /* Map attribute names to pseudo record type. */ if (rec_type == REC_TYPE_ATTR) { if (split_nameval(STR(buf), &attr_name, &attr_value) != 0 || *attr_value == 0) continue; if ((junk = rec_attr_map(attr_name)) != 0) { start = attr_value; rec_type = junk; } } switch (rec_type) { case REC_TYPE_DSN_ORCPT: /* RCPT TO ORCPT parameter */ if (dsn_orcpt != 0) /* can't happen */ myfree(dsn_orcpt); dsn_orcpt = mystrdup(start); break; case REC_TYPE_DSN_NOTIFY: /* RCPT TO NOTIFY parameter */ if (alldig(start) && (junk = atoi(start)) > 0 && DSN_NOTIFY_OK(junk)) dsn_notify = junk; else dsn_notify = 0; break; case REC_TYPE_ORCP: /* unmodified RCPT TO address */ if (orig_rcpt != 0) /* can't happen */ myfree(orig_rcpt); orig_rcpt = mystrdup(start); break; case REC_TYPE_RCPT: /* rewritten RCPT TO address */ if (strcmp(orig_rcpt ? orig_rcpt : start, STR(int_rcpt_buf)) == 0) { if (vstream_fseek(state->dst, curr_offset, SEEK_SET) < 0) { msg_warn("%s: seek file %s: %m", myname, cleanup_path); CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno)); } if (REC_PUT_BUF(state->dst, REC_TYPE_DRCP, buf) < 0) { msg_warn("%s: write queue file: %m", state->queue_id); CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno)); } count++; } if (var_enable_orcpt) /* Matches been_here() call in cleanup_out_recipient(). */ been_here_drop(state->dups, "%s\n%d\n%s\n%s", dsn_orcpt ? dsn_orcpt : "", dsn_notify, orig_rcpt ? orig_rcpt : "", STR(int_rcpt_buf)); /* FALLTHROUGH */ case REC_TYPE_DRCP: /* canceled recipient */ case REC_TYPE_DONE: /* can't happen */ if (orig_rcpt != 0) { myfree(orig_rcpt); orig_rcpt = 0; } if (dsn_orcpt != 0) { myfree(dsn_orcpt); dsn_orcpt = 0; } dsn_notify = 0; break; } } /* Matches been_here_fixed() call in cleanup_out_recipient(). */ if (var_enable_orcpt == 0 && count > 0) been_here_drop_fixed(state->dups, STR(int_rcpt_buf)); if (msg_verbose) msg_info("%s: deleted %d records for recipient \"%s\"", myname, count, ext_rcpt); CLEANUP_DEL_RCPT_RETURN(0); } /* cleanup_repl_body - replace message body */ static const char *cleanup_repl_body(void *context, int cmd, int rec_type, VSTRING *buf) { const char *myname = "cleanup_repl_body"; CLEANUP_STATE *state = (CLEANUP_STATE *) context; static VSTRING empty; /* * XXX Sendmail compatibility: milters don't see the first body line, so * don't expect they will send one. */ switch (cmd) { case MILTER_BODY_LINE: if (cleanup_body_edit_write(state, rec_type, buf) < 0) return (cleanup_milter_error(state, errno)); break; case MILTER_BODY_START: VSTRING_RESET(&empty); if (cleanup_body_edit_start(state) < 0 || cleanup_body_edit_write(state, REC_TYPE_NORM, &empty) < 0) return (cleanup_milter_error(state, errno)); break; case MILTER_BODY_END: if (cleanup_body_edit_finish(state) < 0) return (cleanup_milter_error(state, errno)); break; default: msg_panic("%s: bad command: %d", myname, cmd); } return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, errno)); } /* cleanup_milter_eval - expand macro */ static const char *cleanup_milter_eval(const char *name, void *ptr) { CLEANUP_STATE *state = (CLEANUP_STATE *) ptr; /* * Note: if we use XFORWARD attributes here, then consistency requires * that we forward all Sendmail macros via XFORWARD. */ /* * System macros. */ if (strcmp(name, S8_MAC_DAEMON_NAME) == 0) return (var_milt_daemon_name); if (strcmp(name, S8_MAC_V) == 0) return (var_milt_v); /* * Connect macros. */ #ifndef CLIENT_ATTR_UNKNOWN #define CLIENT_ATTR_UNKNOWN "unknown" #define SERVER_ATTR_UNKNOWN "unknown" #endif if (strcmp(name, S8_MAC__) == 0) { vstring_sprintf(state->temp1, "%s [%s]", state->reverse_name, state->client_addr); if (strcasecmp(state->client_name, state->reverse_name) != 0) vstring_strcat(state->temp1, " (may be forged)"); return (STR(state->temp1)); } if (strcmp(name, S8_MAC_J) == 0) return (var_myhostname); if (strcmp(name, S8_MAC_CLIENT_ADDR) == 0) return (state->client_addr); if (strcmp(name, S8_MAC_CLIENT_NAME) == 0) return (state->client_name); if (strcmp(name, S8_MAC_CLIENT_PORT) == 0) return (state->client_port && strcmp(state->client_port, CLIENT_ATTR_UNKNOWN) ? state->client_port : "0"); if (strcmp(name, S8_MAC_CLIENT_PTR) == 0) return (state->reverse_name); /* XXX S8_MAC_CLIENT_RES needs SMTPD_PEER_CODE_XXX from smtpd. */ if (strcmp(name, S8_MAC_DAEMON_ADDR) == 0) return (state->server_addr); if (strcmp(name, S8_MAC_DAEMON_PORT) == 0) return (state->server_port && strcmp(state->server_port, SERVER_ATTR_UNKNOWN) ? state->server_port : "0"); /* * MAIL FROM macros. */ if (strcmp(name, S8_MAC_I) == 0) return (state->queue_id); #ifdef USE_SASL_AUTH if (strcmp(name, S8_MAC_AUTH_TYPE) == 0) return (nvtable_find(state->attr, MAIL_ATTR_SASL_METHOD)); if (strcmp(name, S8_MAC_AUTH_AUTHEN) == 0) return (nvtable_find(state->attr, MAIL_ATTR_SASL_USERNAME)); if (strcmp(name, S8_MAC_AUTH_AUTHOR) == 0) return (nvtable_find(state->attr, MAIL_ATTR_SASL_SENDER)); #endif if (strcmp(name, S8_MAC_MAIL_ADDR) == 0) return (state->milter_ext_from ? STR(state->milter_ext_from) : 0); /* * RCPT TO macros. */ if (strcmp(name, S8_MAC_RCPT_ADDR) == 0) return (state->milter_ext_rcpt ? STR(state->milter_ext_rcpt) : 0); return (0); } /* cleanup_milter_receive - receive milter instances */ void cleanup_milter_receive(CLEANUP_STATE *state, int count) { if (state->milters) milter_free(state->milters); state->milters = milter_receive(state->src, count); if (state->milters == 0) msg_fatal("cleanup_milter_receive: milter receive failed"); if (count <= 0) return; milter_macro_callback(state->milters, cleanup_milter_eval, (void *) state); milter_edit_callback(state->milters, cleanup_add_header, cleanup_upd_header, cleanup_ins_header, cleanup_del_header, cleanup_chg_from, cleanup_add_rcpt, cleanup_add_rcpt_par, cleanup_del_rcpt, cleanup_repl_body, (void *) state); } /* cleanup_milter_apply - apply Milter response, non-zero if rejecting */ static const char *cleanup_milter_apply(CLEANUP_STATE *state, const char *event, const char *resp) { const char *myname = "cleanup_milter_apply"; const char *action; const char *text; const char *attr; const char *ret = 0; if (msg_verbose) msg_info("%s: %s", myname, resp); /* * Don't process our own milter_header/body checks replies. See comments * in cleanup_milter_hbc_extend(). */ if (cleanup_milter_hbc_reply && strcmp(resp, STR(cleanup_milter_hbc_reply)) == 0) return (0); /* * Don't process Milter replies that are redundant because header/body * checks already decided that we will not receive the message; or Milter * replies that would have conflicting effect with the outcome of * header/body checks (for example, header_checks "discard" action * followed by Milter "reject" reply). Logging both actions would look * silly. */ if (CLEANUP_MILTER_REJECTING_OR_DISCARDING_MESSAGE(state)) { if (msg_verbose) msg_info("%s: ignoring redundant or conflicting milter reply: %s", state->queue_id, resp); return (0); } /* * Sanity check. */ if (state->client_name == 0) msg_panic("%s: missing client info initialization", myname); /* * We don't report errors that were already reported by the content * editing call-back routines. See cleanup_milter_error() above. */ if (CLEANUP_OUT_OK(state) == 0) return (0); switch (resp[0]) { case 'H': /* XXX Should log the reason here. */ if (state->flags & CLEANUP_FLAG_HOLD) return (0); state->flags |= CLEANUP_FLAG_HOLD; action = "milter-hold"; text = "milter triggers HOLD action"; break; case 'D': state->flags |= CLEANUP_FLAG_DISCARD; action = "milter-discard"; text = "milter triggers DISCARD action"; break; case 'S': /* XXX Can this happen after end-of-message? */ state->flags |= CLEANUP_STAT_CONT; action = "milter-reject"; text = cleanup_strerror(CLEANUP_STAT_CONT); break; /* * Override permanent reject with temporary reject. This happens when * the cleanup server has to bounce (hard reject) but is unable to * store the message (soft reject). After a temporary reject we stop * inspecting queue file records, so it can't be overruled by * something else. * * CLEANUP_STAT_CONT and CLEANUP_STAT_DEFER both update the reason * attribute, but CLEANUP_STAT_DEFER takes precedence. It terminates * queue record processing, and prevents bounces from being sent. */ case '4': CLEANUP_MILTER_SET_SMTP_REPLY(state, resp); ret = state->reason; state->errs |= CLEANUP_STAT_DEFER; action = "milter-reject"; text = resp + 4; break; case '5': CLEANUP_MILTER_SET_SMTP_REPLY(state, resp); ret = state->reason; state->errs |= CLEANUP_STAT_CONT; action = "milter-reject"; text = resp + 4; break; default: msg_panic("%s: unexpected mail filter reply: %s", myname, resp); } vstring_sprintf(state->temp1, "%s: %s: %s from %s[%s]: %s;", state->queue_id, action, event, state->client_name, state->client_addr, text); if (state->sender) vstring_sprintf_append(state->temp1, " from=<%s>", info_log_addr_form_sender(state->sender)); if (state->recip) vstring_sprintf_append(state->temp1, " to=<%s>", info_log_addr_form_recipient(state->recip)); if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_PROTO_NAME)) != 0) vstring_sprintf_append(state->temp1, " proto=%s", attr); if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_HELO_NAME)) != 0) vstring_sprintf_append(state->temp1, " helo=<%s>", attr); msg_info("%s", vstring_str(state->temp1)); return (ret); } /* cleanup_milter_client_init - initialize real or ersatz client info */ static void cleanup_milter_client_init(CLEANUP_STATE *state) { static const INET_PROTO_INFO *proto_info; const char *proto_attr; /* * Either the cleanup client specifies a name, address and protocol, or * we have a local submission and pretend localhost/127.0.0.1/AF_INET. */ #define NO_CLIENT_PORT "0" state->client_name = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_NAME); state->reverse_name = nvtable_find(state->attr, MAIL_ATTR_ACT_REVERSE_CLIENT_NAME); state->client_addr = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_ADDR); state->client_port = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_PORT); proto_attr = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_AF); state->server_addr = nvtable_find(state->attr, MAIL_ATTR_ACT_SERVER_ADDR); state->server_port = nvtable_find(state->attr, MAIL_ATTR_ACT_SERVER_PORT); if (state->client_name == 0 || state->client_addr == 0 || proto_attr == 0 || !alldig(proto_attr)) { state->client_name = "localhost"; #ifdef AF_INET6 if (proto_info == 0) proto_info = inet_proto_info(); if (proto_info->sa_family_list[0] == PF_INET6) { state->client_addr = "::1"; state->client_af = AF_INET6; } else #endif { state->client_addr = "127.0.0.1"; state->client_af = AF_INET; } state->server_addr = state->client_addr; } else state->client_af = atoi(proto_attr); if (state->reverse_name == 0) state->reverse_name = state->client_name; /* Compatibility with pre-2.5 queue files. */ if (state->client_port == 0) { state->client_port = NO_CLIENT_PORT; state->server_port = state->client_port; } } /* cleanup_milter_inspect - run message through mail filter */ void cleanup_milter_inspect(CLEANUP_STATE *state, MILTERS *milters) { const char *myname = "cleanup_milter"; const char *resp; if (msg_verbose) msg_info("enter %s", myname); /* * Initialize, in case we're called via smtpd(8). */ if (state->client_name == 0) cleanup_milter_client_init(state); /* * Prologue: prepare for Milter header/body checks. */ if (*var_milt_head_checks) cleanup_milter_header_checks_reinit(state); /* * Process mail filter replies. The reply format is verified by the mail * filter library. */ if ((resp = milter_message(milters, state->handle->stream, state->data_offset, state->auto_hdrs)) != 0) cleanup_milter_apply(state, "END-OF-MESSAGE", resp); /* * Epilogue: finalize Milter header/body checks. */ if (*var_milt_head_checks) cleanup_milter_hbc_finish(state); if (msg_verbose) msg_info("leave %s", myname); } /* cleanup_milter_emul_mail - emulate connect/ehlo/mail event */ void cleanup_milter_emul_mail(CLEANUP_STATE *state, MILTERS *milters, const char *addr) { const char *resp; const char *helo; const char *argv[2]; /* * Per-connection initialization. */ milter_macro_callback(milters, cleanup_milter_eval, (void *) state); milter_edit_callback(milters, cleanup_add_header, cleanup_upd_header, cleanup_ins_header, cleanup_del_header, cleanup_chg_from, cleanup_add_rcpt, cleanup_add_rcpt_par, cleanup_del_rcpt, cleanup_repl_body, (void *) state); if (state->client_name == 0) cleanup_milter_client_init(state); /* * Emulate SMTP events. */ if ((resp = milter_conn_event(milters, state->client_name, state->client_addr, state->client_port, state->client_af)) != 0) { cleanup_milter_apply(state, "CONNECT", resp); return; } #define PRETEND_ESMTP 1 if (CLEANUP_MILTER_OK(state)) { if ((helo = nvtable_find(state->attr, MAIL_ATTR_ACT_HELO_NAME)) == 0) helo = state->client_name; if ((resp = milter_helo_event(milters, helo, PRETEND_ESMTP)) != 0) { cleanup_milter_apply(state, "EHLO", resp); return; } } if (CLEANUP_MILTER_OK(state)) { if (state->milter_ext_from == 0) state->milter_ext_from = vstring_alloc(100); /* Sendmail 8.13 does not externalize the null address. */ if (*addr) quote_821_local(state->milter_ext_from, addr); else vstring_strcpy(state->milter_ext_from, addr); argv[0] = STR(state->milter_ext_from); argv[1] = 0; if ((resp = milter_mail_event(milters, argv)) != 0) { cleanup_milter_apply(state, "MAIL", resp); return; } } } /* cleanup_milter_emul_rcpt - emulate rcpt event */ void cleanup_milter_emul_rcpt(CLEANUP_STATE *state, MILTERS *milters, const char *addr) { const char *myname = "cleanup_milter_emul_rcpt"; const char *resp; const char *argv[2]; /* * Sanity check. */ if (state->client_name == 0) msg_panic("%s: missing client info initialization", myname); /* * CLEANUP_STAT_CONT and CLEANUP_STAT_DEFER both update the reason * attribute, but CLEANUP_STAT_DEFER takes precedence. It terminates * queue record processing, and prevents bounces from being sent. */ if (state->milter_ext_rcpt == 0) state->milter_ext_rcpt = vstring_alloc(100); /* Sendmail 8.13 does not externalize the null address. */ if (*addr) quote_821_local(state->milter_ext_rcpt, addr); else vstring_strcpy(state->milter_ext_rcpt, addr); argv[0] = STR(state->milter_ext_rcpt); argv[1] = 0; if ((resp = milter_rcpt_event(milters, MILTER_FLAG_NONE, argv)) != 0 && cleanup_milter_apply(state, "RCPT", resp) != 0) { msg_warn("%s: milter configuration error: can't reject recipient " "in non-smtpd(8) submission", state->queue_id); msg_warn("%s: message not accepted, try again later", state->queue_id); CLEANUP_MILTER_SET_REASON(state, "4.3.5 Server configuration error"); state->errs |= CLEANUP_STAT_DEFER; } } /* cleanup_milter_emul_data - emulate data event */ void cleanup_milter_emul_data(CLEANUP_STATE *state, MILTERS *milters) { const char *myname = "cleanup_milter_emul_data"; const char *resp; /* * Sanity check. */ if (state->client_name == 0) msg_panic("%s: missing client info initialization", myname); if ((resp = milter_data_event(milters)) != 0) cleanup_milter_apply(state, "DATA", resp); } #ifdef TEST /* * Queue file editing driver for regression tests. In this case it is OK to * report fatal errors after I/O errors. */ #include #include #include #include #include #undef msg_verbose char *cleanup_path; VSTRING *cleanup_trace_path; VSTRING *cleanup_strip_chars; int cleanup_comm_canon_flags; MAPS *cleanup_comm_canon_maps; int cleanup_ext_prop_mask; ARGV *cleanup_masq_domains; int cleanup_masq_flags; MAPS *cleanup_rcpt_bcc_maps; int cleanup_rcpt_canon_flags; MAPS *cleanup_rcpt_canon_maps; MAPS *cleanup_send_bcc_maps; int cleanup_send_canon_flags; MAPS *cleanup_send_canon_maps; int var_dup_filter_limit = DEF_DUP_FILTER_LIMIT; char *var_empty_addr = DEF_EMPTY_ADDR; MAPS *cleanup_virt_alias_maps; char *var_milt_daemon_name = "host.example.com"; char *var_milt_v = DEF_MILT_V; MILTERS *cleanup_milters = (MILTERS *) ((char *) sizeof(*cleanup_milters)); char *var_milt_head_checks = ""; /* Dummies to satisfy unused external references. */ int cleanup_masquerade_internal(CLEANUP_STATE *state, VSTRING *addr, ARGV *masq_domains) { msg_panic("cleanup_masquerade_internal dummy"); } int cleanup_rewrite_internal(const char *context, VSTRING *result, const char *addr) { vstring_strcpy(result, addr); return (0); } int cleanup_map11_internal(CLEANUP_STATE *state, VSTRING *addr, MAPS *maps, int propagate) { msg_panic("cleanup_map11_internal dummy"); } ARGV *cleanup_map1n_internal(CLEANUP_STATE *state, const char *addr, MAPS *maps, int propagate) { msg_panic("cleanup_map1n_internal dummy"); } void cleanup_envelope(CLEANUP_STATE *state, int type, const char *buf, ssize_t len) { msg_panic("cleanup_envelope dummy"); } static void usage(void) { msg_warn("usage:"); msg_warn(" verbose on|off"); msg_warn(" open pathname"); msg_warn(" close"); msg_warn(" add_header index name [value]"); msg_warn(" ins_header index name [value]"); msg_warn(" upd_header index name [value]"); msg_warn(" del_header index name"); msg_warn(" chg_from addr parameters"); msg_warn(" add_rcpt addr"); msg_warn(" add_rcpt_par addr parameters"); msg_warn(" del_rcpt addr"); msg_warn(" replbody pathname"); msg_warn(" header_checks type:name"); } /* flatten_args - unparse partial command line */ static void flatten_args(VSTRING *buf, char **argv) { char **cpp; VSTRING_RESET(buf); for (cpp = argv; *cpp; cpp++) { vstring_strcat(buf, *cpp); if (cpp[1]) VSTRING_ADDCH(buf, ' '); } VSTRING_TERMINATE(buf); } /* open_queue_file - open an unedited queue file (all-zero dummy PTRs) */ static void open_queue_file(CLEANUP_STATE *state, const char *path) { VSTRING *buf = vstring_alloc(100); off_t curr_offset; int rec_type; long msg_seg_len; long data_offset; long rcpt_count; long qmgr_opts; if (state->dst != 0) { msg_warn("closing %s", cleanup_path); vstream_fclose(state->dst); state->dst = 0; myfree(cleanup_path); cleanup_path = 0; } if ((state->dst = vstream_fopen(path, O_RDWR, 0)) == 0) { msg_warn("open %s: %m", path); } else { cleanup_path = mystrdup(path); for (;;) { if ((curr_offset = vstream_ftell(state->dst)) < 0) msg_fatal("file %s: vstream_ftell: %m", cleanup_path); if ((rec_type = rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE)) < 0) msg_fatal("file %s: missing SIZE or PTR record", cleanup_path); if (rec_type == REC_TYPE_SIZE) { if (sscanf(STR(buf), "%ld %ld %ld %ld", &msg_seg_len, &data_offset, &rcpt_count, &qmgr_opts) != 4) msg_fatal("file %s: bad SIZE record: %s", cleanup_path, STR(buf)); state->data_offset = data_offset; state->xtra_offset = data_offset + msg_seg_len; } else if (rec_type == REC_TYPE_FROM) { state->sender_pt_offset = curr_offset; if (LEN(buf) < REC_TYPE_PTR_PAYL_SIZE && rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE) != REC_TYPE_PTR) msg_fatal("file %s: missing PTR record after short sender", cleanup_path); if ((state->sender_pt_target = vstream_ftell(state->dst)) < 0) msg_fatal("file %s: missing END record", cleanup_path); } else if (rec_type == REC_TYPE_PTR) { if (state->data_offset < 0) msg_fatal("file %s: missing SIZE record", cleanup_path); if (curr_offset < state->data_offset || curr_offset > state->xtra_offset) { if (state->append_rcpt_pt_offset < 0) { state->append_rcpt_pt_offset = curr_offset; if (atol(STR(buf)) != 0) msg_fatal("file %s: bad dummy recipient PTR record: %s", cleanup_path, STR(buf)); if ((state->append_rcpt_pt_target = vstream_ftell(state->dst)) < 0) msg_fatal("file %s: vstream_ftell: %m", cleanup_path); } else if (curr_offset > state->xtra_offset && state->append_meta_pt_offset < 0) { state->append_meta_pt_offset = curr_offset; if (atol(STR(buf)) != 0) msg_fatal("file %s: bad dummy meta PTR record: %s", cleanup_path, STR(buf)); if ((state->append_meta_pt_target = vstream_ftell(state->dst)) < 0) msg_fatal("file %s: vstream_ftell: %m", cleanup_path); } } else { if (state->append_hdr_pt_offset < 0) { state->append_hdr_pt_offset = curr_offset; if (atol(STR(buf)) != 0) msg_fatal("file %s: bad dummy header PTR record: %s", cleanup_path, STR(buf)); if ((state->append_hdr_pt_target = vstream_ftell(state->dst)) < 0) msg_fatal("file %s: vstream_ftell: %m", cleanup_path); } } } if (state->append_rcpt_pt_offset > 0 && state->append_hdr_pt_offset > 0 && (rec_type == REC_TYPE_END || state->append_meta_pt_offset > 0)) break; } if (msg_verbose) { msg_info("append_rcpt_pt_offset %ld append_rcpt_pt_target %ld", (long) state->append_rcpt_pt_offset, (long) state->append_rcpt_pt_target); msg_info("append_hdr_pt_offset %ld append_hdr_pt_target %ld", (long) state->append_hdr_pt_offset, (long) state->append_hdr_pt_target); } } vstring_free(buf); } static void close_queue_file(CLEANUP_STATE *state) { (void) vstream_fclose(state->dst); state->dst = 0; myfree(cleanup_path); cleanup_path = 0; } int main(int unused_argc, char **argv) { VSTRING *inbuf = vstring_alloc(100); VSTRING *arg_buf = vstring_alloc(100); char *bufp; int istty = isatty(vstream_fileno(VSTREAM_IN)); CLEANUP_STATE *state = cleanup_state_alloc((VSTREAM *) 0); const char *parens = "{}"; state->queue_id = mystrdup("NOQUEUE"); state->sender = mystrdup("sender"); state->recip = mystrdup("recipient"); state->client_name = "client_name"; state->client_addr = "client_addr"; state->flags |= CLEANUP_FLAG_FILTER_ALL; msg_vstream_init(argv[0], VSTREAM_ERR); var_line_limit = DEF_LINE_LIMIT; var_header_limit = DEF_HEADER_LIMIT; var_enable_orcpt = DEF_ENABLE_ORCPT; var_info_log_addr_form = DEF_INFO_LOG_ADDR_FORM; for (;;) { ARGV *argv; ssize_t index; const char *resp = 0; if (istty) { vstream_printf("- "); vstream_fflush(VSTREAM_OUT); } if (vstring_fgets_nonl(inbuf, VSTREAM_IN) == 0) break; bufp = vstring_str(inbuf); if (!istty) { vstream_printf("> %s\n", bufp); vstream_fflush(VSTREAM_OUT); } if (*bufp == '#' || *bufp == 0 || allspace(bufp)) continue; argv = argv_splitq(bufp, " ", parens); if (argv->argc == 0) { msg_warn("missing command"); } else if (strcmp(argv->argv[0], "?") == 0) { usage(); } else if (strcmp(argv->argv[0], "verbose") == 0) { if (argv->argc != 2) { msg_warn("bad verbose argument count: %ld", (long) argv->argc); } else if (strcmp(argv->argv[1], "on") == 0) { msg_verbose = 2; } else if (strcmp(argv->argv[1], "off") == 0) { msg_verbose = 0; } else { msg_warn("bad verbose argument"); } } else if (strcmp(argv->argv[0], "line_length_limit") == 0) { if (argv->argc != 2) { msg_warn("bad line_length_limit argument count: %ld", (long) argv->argc); } else if (alldig(argv->argv[1]) == 0) { msg_warn("bad line_length_limit argument count: %ld", (long) argv->argc); } else if ((var_line_limit = atoi(argv->argv[1])) < DEF_LINE_LIMIT) { msg_warn("bad line_length_limit argument"); } } else if (strcmp(argv->argv[0], "open") == 0) { if (state->dst != 0) { msg_info("closing %s", VSTREAM_PATH(state->dst)); close_queue_file(state); } if (argv->argc != 2) { msg_warn("bad open argument count: %ld", (long) argv->argc); } else { open_queue_file(state, argv->argv[1]); } } else if (strcmp(argv->argv[0], "enable_original_recipient") == 0) { if (argv->argc == 1) { msg_info("enable_original_recipient: %d", var_enable_orcpt); } else if (argv->argc != 2) { msg_warn("bad enable_original_recipient argument count: %ld", (long) argv->argc); } else if (!alldig(argv->argv[1])) { msg_warn("non-numeric enable_original_recipient argument: %s", argv->argv[1]); } else { var_enable_orcpt = atoi(argv->argv[1]); } } else if (state->dst == 0) { msg_warn("no open queue file"); } else if (strcmp(argv->argv[0], "close") == 0) { if (*var_milt_head_checks) { cleanup_milter_hbc_finish(state); myfree(var_milt_head_checks); var_milt_head_checks = ""; cleanup_milter_header_checks_deinit(); } close_queue_file(state); } else if (cleanup_milter_hbc_reply && LEN(cleanup_milter_hbc_reply)) { /* Postfix libmilter would skip further requests. */ msg_info("ignoring: %s %s %s", argv->argv[0], argv->argc > 1 ? argv->argv[1] : "", argv->argc > 2 ? argv->argv[2] : ""); } else if (strcmp(argv->argv[0], "add_header") == 0) { if (argv->argc < 2) { msg_warn("bad add_header argument count: %ld", (long) argv->argc); } else { flatten_args(arg_buf, argv->argv + 2); resp = cleanup_add_header(state, argv->argv[1], " ", STR(arg_buf)); } } else if (strcmp(argv->argv[0], "ins_header") == 0) { if (argv->argc < 3) { msg_warn("bad ins_header argument count: %ld", (long) argv->argc); } else if ((index = atoi(argv->argv[1])) < 1) { msg_warn("bad ins_header index value"); } else { flatten_args(arg_buf, argv->argv + 3); resp = cleanup_ins_header(state, index, argv->argv[2], " ", STR(arg_buf)); } } else if (strcmp(argv->argv[0], "upd_header") == 0) { if (argv->argc < 3) { msg_warn("bad upd_header argument count: %ld", (long) argv->argc); } else if ((index = atoi(argv->argv[1])) < 1) { msg_warn("bad upd_header index value"); } else { flatten_args(arg_buf, argv->argv + 3); resp = cleanup_upd_header(state, index, argv->argv[2], " ", STR(arg_buf)); } } else if (strcmp(argv->argv[0], "del_header") == 0) { if (argv->argc != 3) { msg_warn("bad del_header argument count: %ld", (long) argv->argc); } else if ((index = atoi(argv->argv[1])) < 1) { msg_warn("bad del_header index value"); } else { cleanup_del_header(state, index, argv->argv[2]); } } else if (strcmp(argv->argv[0], "chg_from") == 0) { if (argv->argc != 3) { msg_warn("bad chg_from argument count: %ld", (long) argv->argc); } else { char *arg = argv->argv[2]; const char *err; if (*arg == parens[0] && (err = extpar(&arg, parens, EXTPAR_FLAG_NONE)) != 0) { msg_warn("%s in \"%s\"", err, arg); } else { cleanup_chg_from(state, argv->argv[1], arg); } } } else if (strcmp(argv->argv[0], "add_rcpt") == 0) { if (argv->argc != 2) { msg_warn("bad add_rcpt argument count: %ld", (long) argv->argc); } else { cleanup_add_rcpt(state, argv->argv[1]); } } else if (strcmp(argv->argv[0], "add_rcpt_par") == 0) { if (argv->argc != 3) { msg_warn("bad add_rcpt_par argument count: %ld", (long) argv->argc); } else { cleanup_add_rcpt_par(state, argv->argv[1], argv->argv[2]); } } else if (strcmp(argv->argv[0], "del_rcpt") == 0) { if (argv->argc != 2) { msg_warn("bad del_rcpt argument count: %ld", (long) argv->argc); } else { cleanup_del_rcpt(state, argv->argv[1]); } } else if (strcmp(argv->argv[0], "replbody") == 0) { if (argv->argc != 2) { msg_warn("bad replbody argument count: %ld", (long) argv->argc); } else { VSTREAM *fp; VSTRING *buf; if ((fp = vstream_fopen(argv->argv[1], O_RDONLY, 0)) == 0) { msg_warn("open %s file: %m", argv->argv[1]); } else { buf = vstring_alloc(100); cleanup_repl_body(state, MILTER_BODY_START, REC_TYPE_NORM, buf); while (vstring_get_nonl(buf, fp) != VSTREAM_EOF) cleanup_repl_body(state, MILTER_BODY_LINE, REC_TYPE_NORM, buf); cleanup_repl_body(state, MILTER_BODY_END, REC_TYPE_NORM, buf); vstring_free(buf); vstream_fclose(fp); } } } else if (strcmp(argv->argv[0], "header_checks") == 0) { if (argv->argc != 2) { msg_warn("bad header_checks argument count: %ld", (long) argv->argc); } else if (*var_milt_head_checks) { msg_warn("can't change header checks"); } else { var_milt_head_checks = mystrdup(argv->argv[1]); cleanup_milter_header_checks_init(); } } else if (strcmp(argv->argv[0], "sender_bcc_maps") == 0) { if (argv->argc != 2) { msg_warn("bad sender_bcc_maps argument count: %ld", (long) argv->argc); } else { if (cleanup_send_bcc_maps) maps_free(cleanup_send_bcc_maps); cleanup_send_bcc_maps = maps_create("sender_bcc_maps", argv->argv[1], DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST); state->flags |= CLEANUP_FLAG_BCC_OK; var_rcpt_delim = ""; } } else { msg_warn("bad command: %s", argv->argv[0]); } argv_free(argv); if (resp) cleanup_milter_apply(state, "END-OF-MESSAGE", resp); } vstring_free(inbuf); vstring_free(arg_buf); if (state->append_meta_pt_offset >= 0) { if (state->flags) msg_info("flags = %s", cleanup_strflags(state->flags)); if (state->errs) msg_info("errs = %s", cleanup_strerror(state->errs)); } cleanup_state_free(state); if (*var_milt_head_checks) myfree(var_milt_head_checks); return (0); } #endif