static void qmqpd_open_file(QMQPD_STATE *state) { int cleanup_flags; /* * Connect to the cleanup server. Log client name/address with queue ID. */ cleanup_flags = input_transp_cleanup(CLEANUP_FLAG_MASK_EXTERNAL, qmqpd_input_transp_mask); state->dest = mail_stream_service(MAIL_CLASS_PUBLIC, var_cleanup_service); if (state->dest == 0 || attr_print(state->dest->stream, ATTR_FLAG_NONE, ATTR_TYPE_INT, MAIL_ATTR_FLAGS, cleanup_flags, ATTR_TYPE_END) != 0) msg_fatal("unable to connect to the %s %s service", MAIL_CLASS_PUBLIC, var_cleanup_service); state->cleanup = state->dest->stream; state->queue_id = mystrdup(state->dest->id); msg_info("%s: client=%s", state->queue_id, state->namaddr); /* * Record the time of arrival. Optionally, enable content filtering (not * bloody likely, but present for the sake of consistency with all other * Postfix points of entrance). */ rec_fprintf(state->cleanup, REC_TYPE_TIME, REC_TYPE_TIME_FORMAT, REC_TYPE_TIME_ARG(state->arrival_time)); if (*var_filter_xport) rec_fprintf(state->cleanup, REC_TYPE_FILT, "%s", var_filter_xport); }
static void post_mail_init(VSTREAM *stream, const char *sender, const char *recipient, int filter_class, int trace_flags, VSTRING *queue_id) { VSTRING *id = queue_id ? queue_id : vstring_alloc(100); struct timeval now; const char *date; int cleanup_flags = int_filt_flags(filter_class) | CLEANUP_FLAG_MASK_INTERNAL; GETTIMEOFDAY(&now); date = mail_date(now.tv_sec); /* * Negotiate with the cleanup service. Give up if we can't agree. */ if (attr_scan(stream, ATTR_FLAG_STRICT, ATTR_TYPE_STR, MAIL_ATTR_QUEUEID, id, ATTR_TYPE_END) != 1 || attr_print(stream, ATTR_FLAG_NONE, ATTR_TYPE_INT, MAIL_ATTR_FLAGS, cleanup_flags, ATTR_TYPE_END) != 0) msg_fatal("unable to contact the %s service", var_cleanup_service); /* * Generate a minimal envelope section. The cleanup service will add a * size record. */ rec_fprintf(stream, REC_TYPE_TIME, REC_TYPE_TIME_FORMAT, REC_TYPE_TIME_ARG(now)); rec_fprintf(stream, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_LOG_ORIGIN, MAIL_ATTR_ORG_LOCAL); rec_fprintf(stream, REC_TYPE_ATTR, "%s=%d", MAIL_ATTR_TRACE_FLAGS, trace_flags); rec_fputs(stream, REC_TYPE_FROM, sender); rec_fputs(stream, REC_TYPE_RCPT, recipient); rec_fputs(stream, REC_TYPE_MESG, ""); /* * Do the Received: and Date: header lines. This allows us to shave a few * cycles by using the expensive date conversion result for both. */ post_mail_fprintf(stream, "Received: by %s (%s)", var_myhostname, var_mail_name); post_mail_fprintf(stream, "\tid %s; %s", vstring_str(id), date); post_mail_fprintf(stream, "Date: %s", date); if (queue_id == 0) vstring_free(id); }
int forward_append(DELIVER_ATTR attr) { FORWARD_INFO *info; HTABLE *table_snd; /* * Sanity checks. */ if (msg_verbose) msg_info("forward delivered=%s sender=%s recip=%s", attr.delivered, attr.sender, attr.rcpt.address); if (forward_dt == 0) msg_panic("forward_append: missing forward_init call"); /* * In order to find the recipient list, first index a table by * delivered-to header address, then by envelope sender address. */ if ((table_snd = (HTABLE *) htable_find(forward_dt, attr.delivered)) == 0) { table_snd = htable_create(0); htable_enter(forward_dt, attr.delivered, (void *) table_snd); } if ((info = (FORWARD_INFO *) htable_find(table_snd, attr.sender)) == 0) { if ((info = forward_open(attr.request, attr.sender)) == 0) return (-1); htable_enter(table_snd, attr.sender, (void *) info); } /* * Append the recipient to the message envelope. Don't send the original * recipient or notification mask if it was reset due to mailing list * expansion. */ if (*attr.rcpt.dsn_orcpt) rec_fprintf(info->cleanup, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_DSN_ORCPT, attr.rcpt.dsn_orcpt); if (attr.rcpt.dsn_notify) rec_fprintf(info->cleanup, REC_TYPE_ATTR, "%s=%d", MAIL_ATTR_DSN_NOTIFY, attr.rcpt.dsn_notify); if (*attr.rcpt.orig_addr) rec_fputs(info->cleanup, REC_TYPE_ORCP, attr.rcpt.orig_addr); rec_fputs(info->cleanup, REC_TYPE_RCPT, attr.rcpt.address); return (vstream_ferror(info->cleanup)); }
static void qmqpd_write_attributes(QMQPD_STATE *state) { /* * Logging attributes, also used for XFORWARD. */ rec_fprintf(state->cleanup, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_LOG_CLIENT_NAME, state->name); rec_fprintf(state->cleanup, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_LOG_CLIENT_ADDR, state->rfc_addr); rec_fprintf(state->cleanup, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_LOG_CLIENT_PORT, state->port); rec_fprintf(state->cleanup, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_LOG_ORIGIN, state->namaddr); rec_fprintf(state->cleanup, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_LOG_PROTO_NAME, state->protocol); /* * For consistency with the smtpd Milter client, we need to provide the * real client attributes to the cleanup Milter client. This does not * matter much with qmqpd which speaks to trusted clients only, but we * want to be sure that the cleanup input protocol is ready when a new * type of network daemon is added to receive mail from the Internet. * * See also the comments in smtpd.c. */ rec_fprintf(state->cleanup, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_ACT_CLIENT_NAME, state->name); rec_fprintf(state->cleanup, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_ACT_CLIENT_ADDR, state->addr); rec_fprintf(state->cleanup, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_ACT_CLIENT_PORT, state->port); rec_fprintf(state->cleanup, REC_TYPE_ATTR, "%s=%u", MAIL_ATTR_ACT_CLIENT_AF, state->addr_family); rec_fprintf(state->cleanup, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_ACT_PROTO_NAME, state->protocol); /* XXX What about the address rewriting context? */ }
void qmgr_message_update_warn(QMGR_MESSAGE *message) { /* * After the "mail delayed" warning, optionally send a "delay cleared" * notification. */ if (qmgr_message_open(message) || vstream_fseek(message->fp, message->warn_offset, SEEK_SET) < 0 || rec_fprintf(message->fp, REC_TYPE_WARN, REC_TYPE_WARN_FORMAT, REC_TYPE_WARN_ARG(-1)) < 0 || vstream_fflush(message->fp)) msg_fatal("update queue file %s: %m", VSTREAM_PATH(message->fp)); qmgr_message_close(message); }
void qmgr_message_update_warn(QMGR_MESSAGE *message) { /* * XXX eventually this should let us schedule multiple warnings, right * now it just allows for one. */ if (qmgr_message_open(message) || vstream_fseek(message->fp, message->warn_offset, SEEK_SET) < 0 || rec_fprintf(message->fp, REC_TYPE_WARN, REC_TYPE_WARN_FORMAT, REC_TYPE_WARN_ARG(0)) < 0 || vstream_fflush(message->fp)) msg_fatal("update queue file %s: %m", VSTREAM_PATH(message->fp)); qmgr_message_close(message); }
static int smtpd_proxy_save_cmd(SMTPD_STATE *state, int expect, const char *fmt,...) { va_list ap; /* * Errors first. */ if (vstream_ferror(smtpd_proxy_replay_stream) || vstream_feof(smtpd_proxy_replay_stream)) return (smtpd_proxy_replay_rdwr_error(state)); /* * Save the expected reply first, so that the replayer can safely * overwrite the input buffer with the command. */ rec_fprintf(smtpd_proxy_replay_stream, REC_TYPE_RCPT, "%d", expect); /* * The command can be omitted at the start of an SMTP session. This is * not documented as part of the official interface because it is used * only internally to this module. Use an explicit null string in case * the SMTPD_PROXY_CONN_FMT implementation details change. */ if (fmt == SMTPD_PROXY_CONN_FMT) fmt = ""; /* * Save the command to the replay log, and send it to the before-queue * filter after we have received the entire message. */ va_start(ap, fmt); rec_vfprintf(smtpd_proxy_replay_stream, REC_TYPE_FROM, fmt, ap); va_end(ap); /* * If we just saved the "." command, replay the log. */ return (strcmp(fmt, ".") ? 0 : smtpd_proxy_replay_send(state)); }
int milter_send(MILTERS *milters, VSTREAM *stream) { MILTER *m; int status = 0; int count = 0; /* * XXX Optimization: send only the filters that are actually used in the * remote process. No point sending a filter that looks at HELO commands * to a cleanup server. For now we skip only the filters that are known * to be disabled (either in global error state or in global accept * state). * * XXX We must send *some* information, even when there are no active * filters, otherwise the cleanup server would try to apply its own * non_smtpd_milters settings. */ if (milters != 0) for (m = milters->milter_list; m != 0; m = m->next) if (m->active(m)) count++; (void) rec_fprintf(stream, REC_TYPE_MILT_COUNT, "%d", count); if (msg_verbose) msg_info("send %d milters", count); /* * XXX Optimization: don't send or receive further information when there * aren't any active filters. */ if (count <= 0) return (0); /* * Send the filter macro name lists. */ (void) attr_print(stream, ATTR_FLAG_MORE, ATTR_TYPE_FUNC, milter_macros_print, (void *) milters->macros, ATTR_TYPE_END); /* * Send the filter instances. */ for (m = milters->milter_list; m != 0; m = m->next) if (m->active(m) && (status = m->send(m, stream)) != 0) break; /* * Over to you. */ if (status != 0 || attr_scan(stream, ATTR_FLAG_STRICT, ATTR_TYPE_INT, MAIL_ATTR_STATUS, &status, ATTR_TYPE_END) != 1 || status != 0) { msg_warn("cannot send milters to service %s", VSTREAM_PATH(stream)); return (-1); } return (0); }
static void enqueue(const int flags, const char *encoding, const char *dsn_envid, int dsn_ret, int dsn_notify, const char *rewrite_context, const char *sender, const char *full_name, char **recipients) { VSTRING *buf; VSTREAM *dst; char *saved_sender; char **cpp; int type; char *start; int skip_from_; TOK822 *tree; TOK822 *tp; int rcpt_count = 0; enum { STRIP_CR_DUNNO, STRIP_CR_DO, STRIP_CR_DONT, STRIP_CR_ERROR } strip_cr; MAIL_STREAM *handle; VSTRING *postdrop_command; uid_t uid = getuid(); int status; int naddr; int prev_type; MIME_STATE *mime_state = 0; SM_STATE state; int mime_errs; const char *errstr; int addr_count; int level; static NAME_CODE sm_fix_eol_table[] = { SM_FIX_EOL_ALWAYS, STRIP_CR_DO, SM_FIX_EOL_STRICT, STRIP_CR_DUNNO, SM_FIX_EOL_NEVER, STRIP_CR_DONT, 0, STRIP_CR_ERROR, }; /* * Access control is enforced in the postdrop command. The code here * merely produces a more user-friendly interface. */ if ((errstr = check_user_acl_byuid(VAR_SUBMIT_ACL, var_submit_acl, uid)) != 0) msg_fatal_status(EX_NOPERM, "User %s(%ld) is not allowed to submit mail", errstr, (long) uid); /* * Initialize. */ buf = vstring_alloc(100); /* * Stop run-away process accidents by limiting the queue file size. This * is not a defense against DOS attack. */ if (var_message_limit > 0 && get_file_limit() > var_message_limit) set_file_limit((off_t) var_message_limit); /* * The sender name is provided by the user. In principle, the mail pickup * service could deduce the sender name from queue file ownership, but: * pickup would not be able to run chrooted, and it may not be desirable * to use login names at all. */ if (sender != 0) { VSTRING_RESET(buf); VSTRING_TERMINATE(buf); tree = tok822_parse(sender); for (naddr = 0, tp = tree; tp != 0; tp = tp->next) if (tp->type == TOK822_ADDR && naddr++ == 0) tok822_internalize(buf, tp->head, TOK822_STR_DEFL); tok822_free_tree(tree); saved_sender = mystrdup(STR(buf)); if (naddr > 1) msg_warn("-f option specified malformed sender: %s", sender); } else { if ((sender = username()) == 0) msg_fatal_status(EX_OSERR, "no login name found for user ID %lu", (unsigned long) uid); saved_sender = mystrdup(sender); } /* * Let the postdrop command open the queue file for us, and sanity check * the content. XXX Make postdrop a manifest constant. */ errno = 0; postdrop_command = vstring_alloc(1000); vstring_sprintf(postdrop_command, "%s/postdrop -r", var_command_dir); for (level = 0; level < msg_verbose; level++) vstring_strcat(postdrop_command, " -v"); if ((handle = mail_stream_command(STR(postdrop_command))) == 0) msg_fatal_status(EX_UNAVAILABLE, "%s(%ld): unable to execute %s: %m", saved_sender, (long) uid, STR(postdrop_command)); vstring_free(postdrop_command); dst = handle->stream; /* * First, write envelope information to the output stream. * * For sendmail compatibility, parse each command-line recipient as if it * were an RFC 822 message header; some MUAs specify comma-separated * recipient lists; and some MUAs even specify "word word <address>". * * Sort-uniq-ing the recipient list is done after address canonicalization, * before recipients are written to queue file. That's cleaner than * having the queue manager nuke duplicate recipient status records. * * XXX Should limit the size of envelope records. * * With "sendmail -N", instead of a per-message NOTIFY record we store one * per recipient so that we can simplify the implementation somewhat. */ if (dsn_envid) rec_fprintf(dst, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_DSN_ENVID, dsn_envid); if (dsn_ret) rec_fprintf(dst, REC_TYPE_ATTR, "%s=%d", MAIL_ATTR_DSN_RET, dsn_ret); rec_fprintf(dst, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_RWR_CONTEXT, rewrite_context); if (full_name || (full_name = fullname()) != 0) rec_fputs(dst, REC_TYPE_FULL, full_name); rec_fputs(dst, REC_TYPE_FROM, saved_sender); if (verp_delims && *saved_sender == 0) msg_fatal_status(EX_USAGE, "%s(%ld): -V option requires non-null sender address", saved_sender, (long) uid); if (encoding) rec_fprintf(dst, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_ENCODING, encoding); if (DEL_REQ_TRACE_FLAGS(flags)) rec_fprintf(dst, REC_TYPE_ATTR, "%s=%d", MAIL_ATTR_TRACE_FLAGS, DEL_REQ_TRACE_FLAGS(flags)); if (verp_delims) rec_fputs(dst, REC_TYPE_VERP, verp_delims); if (recipients) { for (cpp = recipients; *cpp != 0; cpp++) { tree = tok822_parse(*cpp); for (addr_count = 0, tp = tree; tp != 0; tp = tp->next) { if (tp->type == TOK822_ADDR) { tok822_internalize(buf, tp->head, TOK822_STR_DEFL); if (dsn_notify) rec_fprintf(dst, REC_TYPE_ATTR, "%s=%d", MAIL_ATTR_DSN_NOTIFY, dsn_notify); if (REC_PUT_BUF(dst, REC_TYPE_RCPT, buf) < 0) msg_fatal_status(EX_TEMPFAIL, "%s(%ld): error writing queue file: %m", saved_sender, (long) uid); ++rcpt_count; ++addr_count; } } tok822_free_tree(tree); if (addr_count == 0) { if (rec_put(dst, REC_TYPE_RCPT, "", 0) < 0) msg_fatal_status(EX_TEMPFAIL, "%s(%ld): error writing queue file: %m", saved_sender, (long) uid); ++rcpt_count; } } } /* * Append the message contents to the queue file. Write chunks of at most * 1kbyte. Internally, we use different record types for data ending in * LF and for data that doesn't, so we can actually be binary transparent * for local mail. Unfortunately, SMTP has no record continuation * convention, so there is no guarantee that arbitrary data will be * delivered intact via SMTP. Strip leading From_ lines. For the benefit * of UUCP environments, also get rid of leading >>>From_ lines. */ rec_fputs(dst, REC_TYPE_MESG, ""); if (DEL_REQ_TRACE_ONLY(flags) != 0) { if (flags & SM_FLAG_XRCPT) msg_fatal_status(EX_USAGE, "%s(%ld): -t option cannot be used with -bv", saved_sender, (long) uid); if (*saved_sender) rec_fprintf(dst, REC_TYPE_NORM, "From: %s", saved_sender); rec_fprintf(dst, REC_TYPE_NORM, "Subject: probe"); if (recipients) { rec_fprintf(dst, REC_TYPE_CONT, "To:"); for (cpp = recipients; *cpp != 0; cpp++) { rec_fprintf(dst, REC_TYPE_NORM, " %s%s", *cpp, cpp[1] ? "," : ""); } } } else { /* * Initialize the MIME processor and set up the callback context. */ if (flags & SM_FLAG_XRCPT) { state.dst = dst; state.recipients = argv_alloc(2); state.resent_recip = argv_alloc(2); state.resent = 0; state.saved_sender = saved_sender; state.uid = uid; state.temp = vstring_alloc(10); mime_state = mime_state_alloc(MIME_OPT_DISABLE_MIME | MIME_OPT_REPORT_TRUNC_HEADER, output_header, (MIME_STATE_ANY_END) 0, output_text, (MIME_STATE_ANY_END) 0, (MIME_STATE_ERR_PRINT) 0, (void *) &state); } /* * Process header/body lines. */ skip_from_ = 1; strip_cr = name_code(sm_fix_eol_table, NAME_CODE_FLAG_STRICT_CASE, var_sm_fix_eol); if (strip_cr == STRIP_CR_ERROR) msg_fatal_status(EX_USAGE, "invalid %s value: %s", VAR_SM_FIX_EOL, var_sm_fix_eol); for (prev_type = 0; (type = rec_streamlf_get(VSTREAM_IN, buf, var_line_limit)) != REC_TYPE_EOF; prev_type = type) { if (strip_cr == STRIP_CR_DUNNO && type == REC_TYPE_NORM) { if (VSTRING_LEN(buf) > 0 && vstring_end(buf)[-1] == '\r') strip_cr = STRIP_CR_DO; else strip_cr = STRIP_CR_DONT; } if (skip_from_) { if (type == REC_TYPE_NORM) { start = STR(buf); if (strncmp(start + strspn(start, ">"), "From ", 5) == 0) continue; } skip_from_ = 0; } if (strip_cr == STRIP_CR_DO && type == REC_TYPE_NORM) while (VSTRING_LEN(buf) > 0 && vstring_end(buf)[-1] == '\r') vstring_truncate(buf, VSTRING_LEN(buf) - 1); if ((flags & SM_FLAG_AEOF) && prev_type != REC_TYPE_CONT && VSTRING_LEN(buf) == 1 && *STR(buf) == '.') break; if (mime_state) { mime_errs = mime_state_update(mime_state, type, STR(buf), VSTRING_LEN(buf)); if (mime_errs) msg_fatal_status(EX_DATAERR, "%s(%ld): unable to extract recipients: %s", saved_sender, (long) uid, mime_state_error(mime_errs)); } else { if (REC_PUT_BUF(dst, type, buf) < 0) msg_fatal_status(EX_TEMPFAIL, "%s(%ld): error writing queue file: %m", saved_sender, (long) uid); } } } /* * Finish MIME processing. We need a final mime_state_update() call in * order to flush text that is still buffered. That can happen when the * last line did not end in newline. */ if (mime_state) { mime_errs = mime_state_update(mime_state, REC_TYPE_EOF, "", 0); if (mime_errs) msg_fatal_status(EX_DATAERR, "%s(%ld): unable to extract recipients: %s", saved_sender, (long) uid, mime_state_error(mime_errs)); mime_state = mime_state_free(mime_state); } /* * Append recipient addresses that were extracted from message headers. */ rec_fputs(dst, REC_TYPE_XTRA, ""); if (flags & SM_FLAG_XRCPT) { for (cpp = state.resent ? state.resent_recip->argv : state.recipients->argv; *cpp; cpp++) { if (dsn_notify) rec_fprintf(dst, REC_TYPE_ATTR, "%s=%d", MAIL_ATTR_DSN_NOTIFY, dsn_notify); if (rec_put(dst, REC_TYPE_RCPT, *cpp, strlen(*cpp)) < 0) msg_fatal_status(EX_TEMPFAIL, "%s(%ld): error writing queue file: %m", saved_sender, (long) uid); ++rcpt_count; } argv_free(state.recipients); argv_free(state.resent_recip); vstring_free(state.temp); } if (rcpt_count == 0) msg_fatal_status(EX_USAGE, (flags & SM_FLAG_XRCPT) ? "%s(%ld): No recipient addresses found in message header" : "%s(%ld): Recipient addresses must be specified on" " the command line or via the -t option", saved_sender, (long) uid); /* * Identify the end of the queue file. */ rec_fputs(dst, REC_TYPE_END, ""); /* * Make sure that the message makes it to the file system. Once we have * terminated with successful exit status we cannot lose the message due * to "frivolous reasons". If all goes well, prevent the run-time error * handler from removing the file. */ if (vstream_ferror(VSTREAM_IN)) msg_fatal_status(EX_DATAERR, "%s(%ld): error reading input: %m", saved_sender, (long) uid); if ((status = mail_stream_finish(handle, (VSTRING *) 0)) != 0) msg_fatal_status((status & CLEANUP_STAT_BAD) ? EX_SOFTWARE : (status & CLEANUP_STAT_WRITE) ? EX_TEMPFAIL : EX_UNAVAILABLE, "%s(%ld): %s", saved_sender, (long) uid, cleanup_strerror(status)); /* * Don't leave them in the dark. */ if (DEL_REQ_TRACE_FLAGS(flags)) { vstream_printf("Mail Delivery Status Report will be mailed to <%s>.\n", saved_sender); vstream_fflush(VSTREAM_OUT); } /* * Cleanup. Not really necessary as we're about to exit, but good for * debugging purposes. */ vstring_free(buf); myfree(saved_sender); }
static void qmqpd_write_content(QMQPD_STATE *state) { char *start; char *next; int len; int rec_type; int first = 1; int ch; /* * Start the message content segment. Prepend our own Received: header to * the message content. List the recipient only when a message has one * recipient. Otherwise, don't list the recipient to avoid revealing Bcc: * recipients that are supposed to be invisible. */ rec_fputs(state->cleanup, REC_TYPE_MESG, ""); rec_fprintf(state->cleanup, REC_TYPE_NORM, "Received: from %s (%s [%s])", state->name, state->name, state->rfc_addr); if (state->rcpt_count == 1 && state->recipient) { rec_fprintf(state->cleanup, REC_TYPE_NORM, "\tby %s (%s) with %s id %s", var_myhostname, var_mail_name, state->protocol, state->queue_id); quote_822_local(state->buf, state->recipient); rec_fprintf(state->cleanup, REC_TYPE_NORM, "\tfor <%s>; %s", STR(state->buf), mail_date(state->arrival_time.tv_sec)); } else { rec_fprintf(state->cleanup, REC_TYPE_NORM, "\tby %s (%s) with %s", var_myhostname, var_mail_name, state->protocol); rec_fprintf(state->cleanup, REC_TYPE_NORM, "\tid %s; %s", state->queue_id, mail_date(state->arrival_time.tv_sec)); } #ifdef RECEIVED_ENVELOPE_FROM quote_822_local(state->buf, state->sender); rec_fprintf(state->cleanup, REC_TYPE_NORM, "\t(envelope-from <%s>)", STR(state->buf)); #endif /* * Write the message content. * * XXX Force an empty record when the queue file content begins with * whitespace, so that it won't be considered as being part of our own * Received: header. What an ugly Kluge. * * XXX Deal with UNIX-style From_ lines at the start of message content just * in case. */ for (next = STR(state->message); /* void */ ; /* void */ ) { if ((ch = qmqpd_next_line(state->message, &start, &len, &next)) < 0) break; if (ch == '\n') rec_type = REC_TYPE_NORM; else rec_type = REC_TYPE_CONT; if (first) { if (strncmp(start + strspn(start, ">"), "From ", 5) == 0) { rec_fprintf(state->cleanup, rec_type, "X-Mailbox-Line: %.*s", len, start); continue; } first = 0; if (len > 0 && IS_SPACE_TAB(start[0])) rec_put(state->cleanup, REC_TYPE_NORM, "", 0); } if (rec_put(state->cleanup, rec_type, start, len) < 0) { state->err = CLEANUP_STAT_WRITE; return; } } }
int main(int argc, char **argv) { struct stat st; int fd; int c; VSTRING *buf; int status; MAIL_STREAM *dst; int rec_type; static char *segment_info[] = { REC_TYPE_POST_ENVELOPE, REC_TYPE_POST_CONTENT, REC_TYPE_POST_EXTRACT, "" }; char **expected; uid_t uid = getuid(); ARGV *import_env; const char *error_text; char *attr_name; char *attr_value; const char *errstr; char *junk; struct timeval start; int saved_errno; int from_count = 0; int rcpt_count = 0; int validate_input = 1; /* * Fingerprint executables and core dumps. */ MAIL_VERSION_STAMP_ALLOCATE; /* * Be consistent with file permissions. */ umask(022); /* * To minimize confusion, make sure that the standard file descriptors * are open before opening anything else. XXX Work around for 44BSD where * fstat can return EBADF on an open file descriptor. */ for (fd = 0; fd < 3; fd++) if (fstat(fd, &st) == -1 && (close(fd), open("/dev/null", O_RDWR, 0)) != fd) msg_fatal("open /dev/null: %m"); /* * Set up logging. Censor the process name: it is provided by the user. */ argv[0] = "postdrop"; msg_vstream_init(argv[0], VSTREAM_ERR); msg_syslog_init(mail_task("postdrop"), LOG_PID, LOG_FACILITY); set_mail_conf_str(VAR_PROCNAME, var_procname = mystrdup(argv[0])); /* * Check the Postfix library version as soon as we enable logging. */ MAIL_VERSION_CHECK; /* * Parse JCL. This program is set-gid and must sanitize all command-line * arguments. The configuration directory argument is validated by the * mail configuration read routine. Don't do complex things until we have * completed initializations. */ while ((c = GETOPT(argc, argv, "c:rv")) > 0) { switch (c) { case 'c': if (setenv(CONF_ENV_PATH, optarg, 1) < 0) msg_fatal("out of memory"); break; case 'r': /* forward compatibility */ break; case 'v': if (geteuid() == 0) msg_verbose++; break; default: msg_fatal("usage: %s [-c config_dir] [-v]", argv[0]); } } /* * Read the global configuration file and extract configuration * information. Some claim that the user should supply the working * directory instead. That might be OK, given that this command needs * write permission in a subdirectory called "maildrop". However we still * need to reliably detect incomplete input, and so we must perform * record-level I/O. With that, we should also take the opportunity to * perform some sanity checks on the input. */ mail_conf_read(); /* Re-evaluate mail_task() after reading main.cf. */ msg_syslog_init(mail_task("postdrop"), LOG_PID, LOG_FACILITY); get_mail_conf_str_table(str_table); /* * Mail submission access control. Should this be in the user-land gate, * or in the daemon process? */ mail_dict_init(); if ((errstr = check_user_acl_byuid(VAR_SUBMIT_ACL, var_submit_acl, uid)) != 0) msg_fatal("User %s(%ld) is not allowed to submit mail", errstr, (long) uid); /* * Stop run-away process accidents by limiting the queue file size. This * is not a defense against DOS attack. */ if (var_message_limit > 0 && get_file_limit() > var_message_limit) set_file_limit((off_t) var_message_limit); /* * This program is installed with setgid privileges. Strip the process * environment so that we don't have to trust the C library. */ import_env = mail_parm_split(VAR_IMPORT_ENVIRON, var_import_environ); clean_env(import_env->argv); argv_free(import_env); if (chdir(var_queue_dir)) msg_fatal("chdir %s: %m", var_queue_dir); if (msg_verbose) msg_info("chdir %s", var_queue_dir); /* * Set up signal handlers and a runtime error handler so that we can * clean up incomplete output. * * postdrop_sig() uses the in-kernel SIGINT handler address as an atomic * variable to prevent nested postdrop_sig() calls. For this reason, the * SIGINT handler must be configured before other signal handlers are * allowed to invoke postdrop_sig(). */ signal(SIGPIPE, SIG_IGN); signal(SIGXFSZ, SIG_IGN); signal(SIGINT, postdrop_sig); signal(SIGQUIT, postdrop_sig); if (signal(SIGTERM, SIG_IGN) == SIG_DFL) signal(SIGTERM, postdrop_sig); if (signal(SIGHUP, SIG_IGN) == SIG_DFL) signal(SIGHUP, postdrop_sig); msg_cleanup(postdrop_cleanup); /* End of initializations. */ /* * Don't trust the caller's time information. */ GETTIMEOFDAY(&start); /* * Create queue file. mail_stream_file() never fails. Send the queue ID * to the caller. Stash away a copy of the queue file name so we can * clean up in case of a fatal error or an interrupt. */ dst = mail_stream_file(MAIL_QUEUE_MAILDROP, MAIL_CLASS_PUBLIC, var_pickup_service, 0444); attr_print(VSTREAM_OUT, ATTR_FLAG_NONE, SEND_ATTR_STR(MAIL_ATTR_QUEUEID, dst->id), ATTR_TYPE_END); vstream_fflush(VSTREAM_OUT); postdrop_path = mystrdup(VSTREAM_PATH(dst->stream)); /* * Copy stdin to file. The format is checked so that we can recognize * incomplete input and cancel the operation. With the sanity checks * applied here, the pickup daemon could skip format checks and pass a * file descriptor to the cleanup daemon. These are by no means all * sanity checks - the cleanup service and queue manager services will * reject messages that lack required information. * * If something goes wrong, slurp up the input before responding to the * client, otherwise the client will give up after detecting SIGPIPE. * * Allow attribute records if the attribute specifies the MIME body type * (sendmail -B). */ vstream_control(VSTREAM_IN, CA_VSTREAM_CTL_PATH("stdin"), CA_VSTREAM_CTL_END); buf = vstring_alloc(100); expected = segment_info; /* Override time information from the untrusted caller. */ rec_fprintf(dst->stream, REC_TYPE_TIME, REC_TYPE_TIME_FORMAT, REC_TYPE_TIME_ARG(start)); for (;;) { /* Don't allow PTR records. */ rec_type = rec_get_raw(VSTREAM_IN, buf, var_line_limit, REC_FLAG_NONE); if (rec_type == REC_TYPE_EOF) { /* request cancelled */ mail_stream_cleanup(dst); if (remove(postdrop_path)) msg_warn("uid=%ld: remove %s: %m", (long) uid, postdrop_path); else if (msg_verbose) msg_info("remove %s", postdrop_path); myfree(postdrop_path); postdrop_path = 0; exit(0); } if (rec_type == REC_TYPE_ERROR) msg_fatal("uid=%ld: malformed input", (long) uid); if (strchr(*expected, rec_type) == 0) msg_fatal("uid=%ld: unexpected record type: %d", (long) uid, rec_type); if (rec_type == **expected) expected++; /* Override time information from the untrusted caller. */ if (rec_type == REC_TYPE_TIME) continue; /* Check these at submission time instead of pickup time. */ if (rec_type == REC_TYPE_FROM) from_count++; if (rec_type == REC_TYPE_RCPT) rcpt_count++; /* Limit the attribute types that users may specify. */ if (rec_type == REC_TYPE_ATTR) { if ((error_text = split_nameval(vstring_str(buf), &attr_name, &attr_value)) != 0) { msg_warn("uid=%ld: ignoring malformed record: %s: %.200s", (long) uid, error_text, vstring_str(buf)); continue; } #define STREQ(x,y) (strcmp(x,y) == 0) if ((STREQ(attr_name, MAIL_ATTR_ENCODING) && (STREQ(attr_value, MAIL_ATTR_ENC_7BIT) || STREQ(attr_value, MAIL_ATTR_ENC_8BIT) || STREQ(attr_value, MAIL_ATTR_ENC_NONE))) || STREQ(attr_name, MAIL_ATTR_DSN_ENVID) || STREQ(attr_name, MAIL_ATTR_DSN_NOTIFY) || rec_attr_map(attr_name) || (STREQ(attr_name, MAIL_ATTR_RWR_CONTEXT) && (STREQ(attr_value, MAIL_ATTR_RWR_LOCAL) || STREQ(attr_value, MAIL_ATTR_RWR_REMOTE))) || STREQ(attr_name, MAIL_ATTR_TRACE_FLAGS)) { /* XXX */ rec_fprintf(dst->stream, REC_TYPE_ATTR, "%s=%s", attr_name, attr_value); } else { msg_warn("uid=%ld: ignoring attribute record: %.200s=%.200s", (long) uid, attr_name, attr_value); } continue; } if (REC_PUT_BUF(dst->stream, rec_type, buf) < 0) { /* rec_get() errors must not clobber errno. */ saved_errno = errno; while ((rec_type = rec_get_raw(VSTREAM_IN, buf, var_line_limit, REC_FLAG_NONE)) != REC_TYPE_END && rec_type != REC_TYPE_EOF) if (rec_type == REC_TYPE_ERROR) msg_fatal("uid=%ld: malformed input", (long) uid); validate_input = 0; errno = saved_errno; break; } if (rec_type == REC_TYPE_END) break; } vstring_free(buf); /* * As of Postfix 2.7 the pickup daemon discards mail without recipients. * Such mail may enter the maildrop queue when "postsuper -r" is invoked * before the queue manager deletes an already delivered message. Looking * at file ownership is not a good way to make decisions on what mail to * discard. Instead, the pickup server now requires that new submissions * always have at least one recipient record. * * The Postfix sendmail command already rejects mail without recipients. * However, in the future postdrop may receive mail via other programs, * so we add a redundant recipient check here for future proofing. * * The test for the sender address is just for consistency of error * reporting (report at submission time instead of pickup time). Besides * the segment terminator records, there aren't any other mandatory * records in a Postfix submission queue file. */ if (validate_input && (from_count == 0 || rcpt_count == 0)) { status = CLEANUP_STAT_BAD; mail_stream_cleanup(dst); } /* * Finish the file. */ else if ((status = mail_stream_finish(dst, (VSTRING *) 0)) != 0) { msg_warn("uid=%ld: %m", (long) uid); postdrop_cleanup(); } /* * Disable deletion on fatal error before reporting success, so the file * will not be deleted after we have taken responsibility for delivery. */ if (postdrop_path) { junk = postdrop_path; postdrop_path = 0; myfree(junk); } /* * Send the completion status to the caller and terminate. */ attr_print(VSTREAM_OUT, ATTR_FLAG_NONE, SEND_ATTR_INT(MAIL_ATTR_STATUS, status), SEND_ATTR_STR(MAIL_ATTR_WHY, ""), ATTR_TYPE_END); vstream_fflush(VSTREAM_OUT); exit(status); }
static int pickup_copy(VSTREAM *qfile, VSTREAM *cleanup, PICKUP_INFO *info, VSTRING *buf) { time_t now = time((time_t *) 0); int status; char *name; /* * Protect against time-warped time stamps. Warn about mail that has been * queued for an excessive amount of time. Allow for some time drift with * network clients that mount the maildrop remotely - especially clients * that can't get their daylight savings offsets right. */ #define DAY_SECONDS 86400 #define HOUR_SECONDS 3600 if (info->st.st_mtime > now + 2 * HOUR_SECONDS) { msg_warn("%s: message dated %ld seconds into the future", info->id, (long) (info->st.st_mtime - now)); info->st.st_mtime = now; } else if (info->st.st_mtime < now - DAY_SECONDS) { msg_warn("%s: message has been queued for %d days", info->id, (int) ((now - info->st.st_mtime) / DAY_SECONDS)); } /* * Add content inspection transport. See also postsuper(1). */ if (*var_filter_xport) rec_fprintf(cleanup, REC_TYPE_FILT, "%s", var_filter_xport); /* * Copy the message envelope segment. Allow only those records that we * expect to see in the envelope section. The envelope segment must * contain an envelope sender address. */ if ((status = copy_segment(qfile, cleanup, info, buf, REC_TYPE_ENVELOPE)) != 0) return (status); if (info->sender == 0) { msg_warn("%s: uid=%ld: no envelope sender", info->id, (long) info->st.st_uid); return (REMOVE_MESSAGE_FILE); } /* * For messages belonging to $mail_owner also log the maildrop queue id. * This supports message tracking for mail requeued via "postsuper -r". */ #define MAIL_IS_REQUEUED(info) \ ((info)->st.st_uid == var_owner_uid && ((info)->st.st_mode & S_IROTH) == 0) if (MAIL_IS_REQUEUED(info)) { msg_info("%s: uid=%d from=<%s> orig_id=%s", info->id, (int) info->st.st_uid, info->sender, ((name = strrchr(info->path, '/')) != 0 ? name + 1 : info->path)); } else { msg_info("%s: uid=%d from=<%s>", info->id, (int) info->st.st_uid, info->sender); } /* * Message content segment. Send a dummy message length. Prepend a * Received: header to the message contents. For tracing purposes, * include the message file ownership, without revealing the login name. */ rec_fputs(cleanup, REC_TYPE_MESG, ""); rec_fprintf(cleanup, REC_TYPE_NORM, "Received: by %s (%s, from userid %ld)", var_myhostname, var_mail_name, (long) info->st.st_uid); rec_fprintf(cleanup, REC_TYPE_NORM, "\tid %s; %s", info->id, mail_date(info->st.st_mtime)); /* * Copy the message content segment. Allow only those records that we * expect to see in the message content section. */ if ((status = copy_segment(qfile, cleanup, info, buf, REC_TYPE_CONTENT)) != 0) return (status); /* * Send the segment with information extracted from message headers. * Permit a non-empty extracted segment, so that list manager software * can to output recipients after the message, and so that sysadmins can * re-inject messages after a change of configuration. */ rec_fputs(cleanup, REC_TYPE_XTRA, ""); if ((status = copy_segment(qfile, cleanup, info, buf, REC_TYPE_EXTRACT)) != 0) return (status); /* * There are no errors. Send the end-of-data marker, and get the cleanup * service completion status. XXX Since the pickup service is unable to * bounce, the cleanup service can report only soft errors here. */ rec_fputs(cleanup, REC_TYPE_END, ""); if (attr_scan(cleanup, ATTR_FLAG_MISSING, RECV_ATTR_INT(MAIL_ATTR_STATUS, &status), RECV_ATTR_STR(MAIL_ATTR_WHY, buf), ATTR_TYPE_END) != 2) return (cleanup_service_error(info, CLEANUP_STAT_WRITE)); /* * Depending on the cleanup service completion status, delete the message * file, or try again later. Bounces are dealt with by the cleanup * service itself. The master process wakes up the cleanup service every * now and then. */ if (status) { return (cleanup_service_error_reason(info, status, vstring_str(buf))); } else { return (REMOVE_MESSAGE_FILE); } }
static int copy_segment(VSTREAM *qfile, VSTREAM *cleanup, PICKUP_INFO *info, VSTRING *buf, char *expected) { int type; int check_first = (*expected == REC_TYPE_CONTENT[0]); int time_seen = 0; char *attr_name; char *attr_value; char *saved_attr; int skip_attr; /* * Limit the input record size. All front-end programs should protect the * mail system against unreasonable inputs. This also requires that we * limit the size of envelope records written by the local posting agent. * * Records with named attributes are filtered by postdrop(1). * * We must allow PTR records here because of "postsuper -r". */ for (;;) { if ((type = rec_get(qfile, buf, var_line_limit)) < 0 || strchr(expected, type) == 0) return (file_read_error(info, type)); if (msg_verbose) msg_info("%s: read %c %s", info->id, type, vstring_str(buf)); if (type == *expected) break; if (type == REC_TYPE_FROM) { if (info->sender == 0) info->sender = mystrdup(vstring_str(buf)); /* Compatibility with Postfix < 2.3. */ if (time_seen == 0) rec_fprintf(cleanup, REC_TYPE_TIME, "%ld", (long) info->st.st_mtime); } if (type == REC_TYPE_TIME) time_seen = 1; /* * XXX Workaround: REC_TYPE_FILT (used in envelopes) == REC_TYPE_CONT * (used in message content). * * As documented in postsuper(1), ignore content filter record. */ if (*expected != REC_TYPE_CONTENT[0]) { if (type == REC_TYPE_FILT) /* Discard FILTER record after "postsuper -r". */ continue; if (type == REC_TYPE_RDR) /* Discard REDIRECT record after "postsuper -r". */ continue; } if (*expected == REC_TYPE_EXTRACT[0]) { if (type == REC_TYPE_RRTO) /* Discard return-receipt record after "postsuper -r". */ continue; if (type == REC_TYPE_ERTO) /* Discard errors-to record after "postsuper -r". */ continue; if (type == REC_TYPE_ATTR) { saved_attr = mystrdup(vstring_str(buf)); skip_attr = (split_nameval(saved_attr, &attr_name, &attr_value) == 0 && rec_attr_map(attr_name) == 0); myfree(saved_attr); /* Discard other/header/body action after "postsuper -r". */ if (skip_attr) continue; } } /* * XXX Force an empty record when the queue file content begins with * whitespace, so that it won't be considered as being part of our * own Received: header. What an ugly Kluge. */ if (check_first && (type == REC_TYPE_NORM || type == REC_TYPE_CONT)) { check_first = 0; if (VSTRING_LEN(buf) > 0 && IS_SPACE_TAB(vstring_str(buf)[0])) rec_put(cleanup, REC_TYPE_NORM, "", 0); } if ((REC_PUT_BUF(cleanup, type, buf)) < 0) return (cleanup_service_error(info, CLEANUP_STAT_WRITE)); } return (0); }
static int forward_send(FORWARD_INFO *info, DELIVER_REQUEST *request, DELIVER_ATTR attr, char *delivered) { const char *myname = "forward_send"; VSTRING *buffer = vstring_alloc(100); VSTRING *folded; int status; int rec_type = 0; /* * Start the message content segment. Prepend our Delivered-To: header to * the message data. Stop at the first error. XXX Rely on the front-end * services to enforce record size limits. */ rec_fputs(info->cleanup, REC_TYPE_MESG, ""); vstring_strcpy(buffer, delivered); rec_fprintf(info->cleanup, REC_TYPE_NORM, "Received: by %s (%s)", var_myhostname, var_mail_name); rec_fprintf(info->cleanup, REC_TYPE_NORM, "\tid %s; %s", info->queue_id, mail_date(info->posting_time.tv_sec)); if (local_deliver_hdr_mask & DELIVER_HDR_FWD) { folded = vstring_alloc(100); rec_fprintf(info->cleanup, REC_TYPE_NORM, "Delivered-To: %s", casefold(folded, (STR(buffer)))); vstring_free(folded); } if ((status = vstream_ferror(info->cleanup)) == 0) if (vstream_fseek(attr.fp, attr.offset, SEEK_SET) < 0) msg_fatal("%s: seek queue file %s: %m:", myname, VSTREAM_PATH(attr.fp)); while (status == 0 && (rec_type = rec_get(attr.fp, buffer, 0)) > 0) { if (rec_type != REC_TYPE_CONT && rec_type != REC_TYPE_NORM) break; status = (REC_PUT_BUF(info->cleanup, rec_type, buffer) != rec_type); } if (status == 0 && rec_type != REC_TYPE_XTRA) { msg_warn("%s: bad record type: %d in message content", info->queue_id, rec_type); status |= mark_corrupt(attr.fp); } /* * Send the end-of-data marker only when there were no errors. */ if (status == 0) { rec_fputs(info->cleanup, REC_TYPE_XTRA, ""); rec_fputs(info->cleanup, REC_TYPE_END, ""); } /* * Retrieve the cleanup service completion status only if there are no * problems. */ if (status == 0) if (vstream_fflush(info->cleanup) || attr_scan(info->cleanup, ATTR_FLAG_MISSING, RECV_ATTR_INT(MAIL_ATTR_STATUS, &status), ATTR_TYPE_END) != 1) status = 1; /* * Log successful forwarding. * * XXX DSN alias and .forward expansion already report SUCCESS, so don't do * it again here. */ if (status == 0) { attr.rcpt.dsn_notify = (attr.rcpt.dsn_notify == DSN_NOTIFY_SUCCESS ? DSN_NOTIFY_NEVER : attr.rcpt.dsn_notify & ~DSN_NOTIFY_SUCCESS); dsb_update(attr.why, "2.0.0", "relayed", DSB_SKIP_RMTA, DSB_SKIP_REPLY, "forwarded as %s", info->queue_id); status = sent(BOUNCE_FLAGS(request), SENT_ATTR(attr)); } /* * Cleanup. */ vstring_free(buffer); return (status); }
static FORWARD_INFO *forward_open(DELIVER_REQUEST *request, const char *sender) { VSTRING *buffer = vstring_alloc(100); FORWARD_INFO *info; VSTREAM *cleanup; #define FORWARD_OPEN_RETURN(res) do { \ vstring_free(buffer); \ return (res); \ } while (0) /* * Contact the cleanup service and save the new mail queue id. Request * that the cleanup service bounces bad messages to the sender so that we * can avoid the trouble of bounce management. * * In case you wonder what kind of bounces, examples are "too many hops", * "message too large", perhaps some others. The reason not to bounce * ourselves is that we don't really know who the recipients are. */ cleanup = mail_connect(MAIL_CLASS_PUBLIC, var_cleanup_service, BLOCKING); if (cleanup == 0) { msg_warn("connect to %s/%s: %m", MAIL_CLASS_PUBLIC, var_cleanup_service); FORWARD_OPEN_RETURN(0); } close_on_exec(vstream_fileno(cleanup), CLOSE_ON_EXEC); if (attr_scan(cleanup, ATTR_FLAG_STRICT, RECV_ATTR_STR(MAIL_ATTR_QUEUEID, buffer), ATTR_TYPE_END) != 1) { vstream_fclose(cleanup); FORWARD_OPEN_RETURN(0); } info = (FORWARD_INFO *) mymalloc(sizeof(FORWARD_INFO)); info->cleanup = cleanup; info->queue_id = mystrdup(STR(buffer)); GETTIMEOFDAY(&info->posting_time); #define FORWARD_CLEANUP_FLAGS \ (CLEANUP_FLAG_BOUNCE | CLEANUP_FLAG_MASK_INTERNAL \ | smtputf8_autodetect(MAIL_SRC_MASK_FORWARD) \ | ((request->smtputf8 & SMTPUTF8_FLAG_REQUESTED) ? \ CLEANUP_FLAG_SMTPUTF8 : 0)) attr_print(cleanup, ATTR_FLAG_NONE, SEND_ATTR_INT(MAIL_ATTR_FLAGS, FORWARD_CLEANUP_FLAGS), ATTR_TYPE_END); /* * Send initial message envelope information. For bounces, set the * designated sender: mailing list owner, posting user, whatever. */ rec_fprintf(cleanup, REC_TYPE_TIME, REC_TYPE_TIME_FORMAT, REC_TYPE_TIME_ARG(info->posting_time)); rec_fputs(cleanup, REC_TYPE_FROM, sender); /* * Don't send the original envelope ID or full/headers return mask if it * was reset due to mailing list expansion. */ if (request->dsn_ret) rec_fprintf(cleanup, REC_TYPE_ATTR, "%s=%d", MAIL_ATTR_DSN_RET, request->dsn_ret); if (request->dsn_envid && *(request->dsn_envid)) rec_fprintf(cleanup, REC_TYPE_ATTR, "%s=%s", MAIL_ATTR_DSN_ENVID, request->dsn_envid); /* * Zero-length attribute values are place holders for unavailable * attribute values. See qmgr_message.c. They are not meant to be * propagated to queue files. */ #define PASS_ATTR(fp, name, value) do { \ if ((value) && *(value)) \ rec_fprintf((fp), REC_TYPE_ATTR, "%s=%s", (name), (value)); \ } while (0) /* * XXX encapsulate these as one object. */ PASS_ATTR(cleanup, MAIL_ATTR_LOG_CLIENT_NAME, request->client_name); PASS_ATTR(cleanup, MAIL_ATTR_LOG_CLIENT_ADDR, request->client_addr); PASS_ATTR(cleanup, MAIL_ATTR_LOG_PROTO_NAME, request->client_proto); PASS_ATTR(cleanup, MAIL_ATTR_LOG_HELO_NAME, request->client_helo); PASS_ATTR(cleanup, MAIL_ATTR_SASL_METHOD, request->sasl_method); PASS_ATTR(cleanup, MAIL_ATTR_SASL_USERNAME, request->sasl_username); PASS_ATTR(cleanup, MAIL_ATTR_SASL_SENDER, request->sasl_sender); PASS_ATTR(cleanup, MAIL_ATTR_LOG_IDENT, request->log_ident); PASS_ATTR(cleanup, MAIL_ATTR_RWR_CONTEXT, request->rewrite_context); FORWARD_OPEN_RETURN(info); }