diff options
Diffstat (limited to 'src/smtpstone/smtp-sink.c')
-rw-r--r-- | src/smtpstone/smtp-sink.c | 1643 |
1 files changed, 1643 insertions, 0 deletions
diff --git a/src/smtpstone/smtp-sink.c b/src/smtpstone/smtp-sink.c new file mode 100644 index 0000000..c34f21d --- /dev/null +++ b/src/smtpstone/smtp-sink.c @@ -0,0 +1,1643 @@ +/*++ +/* NAME +/* smtp-sink 1 +/* SUMMARY +/* parallelized SMTP/LMTP test server +/* SYNOPSIS +/* .fi +/* \fBsmtp-sink\fR [\fIoptions\fR] [\fBinet:\fR][\fIhost\fR]:\fIport\fR +/* \fIbacklog\fR +/* +/* \fBsmtp-sink\fR [\fIoptions\fR] \fBunix:\fR\fIpathname\fR \fIbacklog\fR +/* DESCRIPTION +/* \fBsmtp-sink\fR listens on the named host (or address) and port. +/* It takes SMTP messages from the network and throws them away. +/* The purpose is to measure client performance, not protocol +/* compliance. +/* +/* \fBsmtp-sink\fR may also be configured to capture each mail +/* delivery transaction to file. Since disk latencies are large +/* compared to network delays, this mode of operation can +/* reduce the maximal performance by several orders of magnitude. +/* +/* Connections can be accepted on IPv4 or IPv6 endpoints, or on +/* UNIX-domain sockets. +/* IPv4 and IPv6 are the default. +/* This program is the complement of the \fBsmtp-source\fR(1) program. +/* +/* Note: this is an unsupported test program. No attempt is made +/* to maintain compatibility between successive versions. +/* +/* Arguments: +/* .IP \fB-4\fR +/* Support IPv4 only. This option has no effect when +/* Postfix is built without IPv6 support. +/* .IP \fB-6\fR +/* Support IPv6 only. This option is not available when +/* Postfix is built without IPv6 support. +/* .IP \fB-8\fR +/* Do not announce 8BITMIME support. +/* .IP \fB-a\fR +/* Do not announce SASL authentication support. +/* .IP "\fB-A \fIdelay\fR" +/* Wait \fIdelay\fR seconds after responding to DATA, then +/* abort prematurely with a 550 reply status. Do not read +/* further input from the client; this is an attempt to block +/* the client before it sends ".". Specify a zero delay value +/* to abort immediately. +/* .IP "\fB-b \fIsoft-bounce-reply\fR" +/* Use \fIsoft-bounce-reply\fR for soft reject responses. The +/* default reply is "450 4.3.0 Error: command failed". +/* .IP "\fB-B \fIhard-bounce-reply\fR" +/* Use \fIhard-bounce-reply\fR for hard reject responses. The +/* default reply is "500 5.3.0 Error: command failed". +/* .IP \fB-c\fR +/* Display running counters that are updated whenever an SMTP +/* session ends, a QUIT command is executed, or when "." is +/* received. +/* .IP \fB-C\fR +/* Disable XCLIENT support. +/* .IP "\fB-d \fIdump-template\fR" +/* Dump each mail transaction to a single-message file whose +/* name is created by expanding the \fIdump-template\fR via +/* strftime(3) and appending a pseudo-random hexadecimal number +/* (example: "%Y%m%d%H/%M." expands into "2006081203/05.809a62e3"). +/* If the template contains "/" characters, missing directories +/* are created automatically. The message dump format is +/* described below. +/* .sp +/* Note: this option keeps one capture file open for every +/* mail transaction in progress. +/* .IP "\fB-D \fIdump-template\fR" +/* Append mail transactions to a multi-message dump file whose +/* name is created by expanding the \fIdump-template\fR via +/* strftime(3). +/* If the template contains "/" characters, missing directories +/* are created automatically. The message dump format is +/* described below. +/* .sp +/* Note: this option keeps one capture file open for every +/* mail transaction in progress. +/* .IP \fB-e\fR +/* Do not announce ESMTP support. +/* .IP \fB-E\fR +/* Do not announce ENHANCEDSTATUSCODES support. +/* .IP "\fB-f \fIcommand,command,...\fR" +/* Reject the specified commands with a hard (5xx) error code. +/* This option implies \fB-p\fR. +/* .sp +/* Examples of commands are CONNECT, HELO, EHLO, LHLO, MAIL, RCPT, VRFY, +/* DATA, ., RSET, NOOP, and QUIT. Separate command names by +/* white space or commas, and use quotes to protect white space +/* from the shell. Command names are case-insensitive. +/* .IP \fB-F\fR +/* Disable XFORWARD support. +/* .IP "\fB-h\fI hostname\fR" +/* Use \fIhostname\fR in the SMTP greeting, in the HELO response, +/* and in the EHLO response. The default hostname is "smtp-sink". +/* .IP "\fB-H\fI delay\fR" +/* Delay the first read operation after receiving DATA (time +/* in seconds). Combine with a large test message and a small +/* TCP window size (see the \fB-T\fR option) to test the Postfix +/* client write_wait() implementation. +/* .IP \fB-L\fR +/* Enable LMTP instead of SMTP. +/* .IP "\fB-m \fIcount\fR (default: 256)" +/* An upper bound on the maximal number of simultaneous +/* connections that \fBsmtp-sink\fR will handle. This prevents +/* the process from running out of file descriptors. Excess +/* connections will stay queued in the TCP/IP stack. +/* .IP "\fB-M \fIcount\fR" +/* Terminate after receiving \fIcount\fR messages. +/* .IP "\fB-n \fIcount\fR" +/* Terminate after \fIcount\fR sessions. +/* .IP \fB-N\fR +/* Do not announce support for DSN. +/* .IP \fB-p\fR +/* Do not announce support for ESMTP command pipelining. +/* .IP \fB-P\fR +/* Change the server greeting so that it appears to come through +/* a CISCO PIX system. Implies \fB-e\fR. +/* .IP "\fB-q \fIcommand,command,...\fR" +/* Disconnect (without replying) after receiving one of the +/* specified commands. +/* .sp +/* Examples of commands are CONNECT, HELO, EHLO, LHLO, MAIL, RCPT, VRFY, +/* DATA, ., RSET, NOOP, and QUIT. Separate command names by +/* white space or commas, and use quotes to protect white space +/* from the shell. Command names are case-insensitive. +/* .IP "\fB-Q \fIcommand,command,...\fR" +/* Send a 421 reply and disconnect after receiving one +/* of the specified commands. +/* .sp +/* Examples of commands are CONNECT, HELO, EHLO, LHLO, MAIL, RCPT, VRFY, +/* DATA, ., RSET, NOOP, and QUIT. Separate command names by +/* white space or commas, and use quotes to protect white space +/* from the shell. Command names are case-insensitive. +/* .IP "\fB-r \fIcommand,command,...\fR" +/* Reject the specified commands with a soft (4xx) error code. +/* This option implies \fB-p\fR. +/* .sp +/* Examples of commands are CONNECT, HELO, EHLO, LHLO, MAIL, RCPT, VRFY, +/* DATA, ., RSET, NOOP, and QUIT. Separate command names by +/* white space or commas, and use quotes to protect white space +/* from the shell. Command names are case-insensitive. +/* .IP "\fB-R \fIroot-directory\fR" +/* Change the process root directory to the specified location. +/* This option requires super-user privileges. See also the +/* \fB-u\fR option. +/* .IP "\fB-s \fIcommand,command,...\fR" +/* Log the named commands to syslogd. +/* .sp +/* Examples of commands are CONNECT, HELO, EHLO, LHLO, MAIL, RCPT, VRFY, +/* DATA, ., RSET, NOOP, and QUIT. Separate command names by +/* white space or commas, and use quotes to protect white space +/* from the shell. Command names are case-insensitive. +/* .IP "\fB-S start-string\fR" +/* An optional string that is prepended to each message that is +/* written to a dump file (see the dump file format description +/* below). The following C escape sequences are supported: \ea +/* (bell), \eb (backspace), \ef (formfeed), \en (newline), \er +/* (carriage return), \et (horizontal tab), \ev (vertical tab), +/* \e\fIddd\fR (up to three octal digits) and \e\e (the backslash +/* character). +/* .IP "\fB-t \fItimeout\fR (default: 100)" +/* Limit the time for receiving a command or sending a response. +/* The time limit is specified in seconds. +/* .IP "\fB-T \fIwindowsize\fR" +/* Override the default TCP window size. To work around +/* broken TCP window scaling implementations, specify a +/* value > 0 and < 65536. +/* .IP "\fB-u \fIusername\fR" +/* Switch to the specified user privileges after opening the +/* network socket and optionally changing the process root +/* directory. This option is required when the process runs +/* with super-user privileges. See also the \fB-R\fR option. +/* .IP \fB-v\fR +/* Show the SMTP conversations. +/* .IP "\fB-w \fIdelay\fR" +/* Wait \fIdelay\fR seconds before responding to a DATA command. +/* .IP "\fB-W \fIcommand:delay[:odds]\fR" +/* Wait \fIdelay\fR seconds before responding to \fIcommand\fR. +/* If \fIodds\fR is also specified (a number between 1-99 +/* inclusive), wait for a random multiple of \fIdelay\fR. The +/* random multiplier is equal to the number of times the program +/* needs to roll a dice with a range of 0..99 inclusive, before +/* the dice produces a result greater than or equal to \fIodds\fR. +/* .IP [\fBinet:\fR][\fIhost\fR]:\fIport\fR +/* Listen on network interface \fIhost\fR (default: any interface) +/* TCP port \fIport\fR. Both \fIhost\fR and \fIport\fR may be +/* specified in numeric or symbolic form. +/* .IP \fBunix:\fR\fIpathname\fR +/* Listen on the UNIX-domain socket at \fIpathname\fR. +/* .IP \fIbacklog\fR +/* The maximum length of the queue of pending connections, +/* as defined by the \fBlisten\fR(2) system call. +/* DUMP FILE FORMAT +/* .ad +/* .fi +/* Each dumped message contains a sequence of text lines, +/* terminated with the newline character. The sequence of +/* information is as follows: +/* .IP \(bu +/* The optional string specified with the \fB-S\fR option. +/* .IP \(bu +/* The \fBsmtp-sink\fR generated headers as documented below. +/* .IP \(bu +/* The message header and body as received from the SMTP client. +/* .IP \(bu +/* An empty line. +/* .PP +/* The format of the \fBsmtp-sink\fR generated headers is as +/* follows: +/* .IP "\fBX-Client-Addr: \fItext\fR" +/* The client IP address without enclosing []. An IPv6 address +/* is prefixed with "ipv6:". This record is always present. +/* .IP "\fBX-Client-Proto: \fItext\fR" +/* The client protocol: SMTP, ESMTP or LMTP. This record is +/* always present. +/* .IP "\fBX-Helo-Args: \fItext\fR" +/* The arguments of the last HELO or EHLO command before this +/* mail delivery transaction. This record is present only if +/* the client sent a recognizable HELO or EHLO command before +/* the DATA command. +/* .IP "\fBX-Mail-Args: \fItext\fR" +/* The arguments of the MAIL command that started this mail +/* delivery transaction. This record is present exactly once. +/* .IP "\fBX-Rcpt-Args: \fItext\fR" +/* The arguments of an RCPT command within this mail delivery +/* transaction. There is one record for each RCPT command, and +/* they are in the order as sent by the client. +/* .IP "\fBReceived: \fItext\fR" +/* A message header for compatibility with mail processing +/* software. This three-line header marks the end of the headers +/* provided by \fBsmtp-sink\fR, and is formatted as follows: +/* .RS +/* .IP "\fBfrom \fIhelo\fR ([\fIaddr\fR])" +/* The HELO or EHLO command argument and client IP address. +/* If the client did not send HELO or EHLO, the client IP +/* address is used instead. +/* .IP "\fBby \fIhost\fB (smtp-sink) with \fIproto\fB id \fIrandom\fB;\fR" +/* The hostname specified with the \fB-h\fR option, the client +/* protocol (see \fBX-Client-Proto\fR above), and the pseudo-random +/* portion of the per-message capture file name. +/* .IP \fItime-stamp\fR +/* A time stamp as defined in RFC 2822. +/* .RE +/* SEE ALSO +/* smtp-source(1), SMTP/LMTP message generator +/* 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 <sys_defs.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <sys/stat.h> +#include <unistd.h> +#include <string.h> +#include <stdlib.h> +#include <fcntl.h> +#include <syslog.h> +#include <signal.h> +#include <time.h> +#include <ctype.h> + +#ifdef STRCASECMP_IN_STRINGS_H +#include <strings.h> +#endif + +/* Utility library. */ + +#include <msg.h> +#include <vstring.h> +#include <vstream.h> +#include <vstring_vstream.h> +#include <get_hostname.h> +#include <listen.h> +#include <events.h> +#include <mymalloc.h> +#include <iostuff.h> +#include <msg_vstream.h> +#include <stringops.h> +#include <sane_accept.h> +#include <inet_proto.h> +#include <myaddrinfo.h> +#include <make_dirs.h> +#include <myrand.h> +#include <chroot_uid.h> + +/* Global library. */ + +#include <smtp_stream.h> +#include <mail_date.h> +#include <mail_version.h> + +/* Application-specific. */ + +typedef struct SINK_STATE { + VSTREAM *stream; + VSTRING *buffer; + int data_state; + int (*read_fn) (struct SINK_STATE *); + int in_mail; + int rcpts; + char *push_back_ptr; + /* Capture file information for fake Received: header */ + MAI_HOSTADDR_STR client_addr; /* IP address */ + char *addr_prefix; /* ipv6: or empty */ + char *helo_args; /* text after HELO or EHLO */ + const char *client_proto; /* SMTP, ESMTP, LMTP */ + time_t start_time; /* MAIL command time */ + int id; /* pseudo-random */ + VSTREAM *dump_file; /* dump file or null */ + void (*delayed_response) (struct SINK_STATE *state, const char *); + char *delayed_args; +} SINK_STATE; + +#define ST_ANY 0 +#define ST_CR 1 +#define ST_CR_LF 2 +#define ST_CR_LF_DOT 3 +#define ST_CR_LF_DOT_CR 4 +#define ST_CR_LF_DOT_CR_LF 5 + +#define PUSH_BACK_PEEK(state) (*(state)->push_back_ptr != 0) +#define PUSH_BACK_GET(state) (*(state)->push_back_ptr++) +#define PUSH_BACK_SET(state, text) ((state)->push_back_ptr = (text)) + +#ifndef DEF_MAX_CLIENT_COUNT +#define DEF_MAX_CLIENT_COUNT 256 +#endif + +#define SOFT_ERROR_RESP "450 4.3.0 Error: command failed" +#define HARD_ERROR_RESP "500 5.3.0 Error: command failed" + + /* + * We can't rely on vstream auto-flushing, so we have to prepare for the + * next read request. + */ +#define SMTP_FLUSH(fp) do { \ + if (vstream_peek(fp) <= 0 && readable(vstream_fileno(fp)) <= 0) \ + smtp_flush(fp); \ + } while (0) + +static int var_tmout = 100; +static int var_max_line_length = 2048; +static char *var_myhostname; +static char *soft_error_resp = SOFT_ERROR_RESP; +static char *hard_error_resp = HARD_ERROR_RESP; +static int command_read(SINK_STATE *); +static int data_read(SINK_STATE *); +static void disconnect(SINK_STATE *); +static void read_timeout(int, void *); +static void read_event(int, void *); +static int show_count; +static int sess_count; +static int quit_count; +static int mesg_count; +static int max_quit_count; +static int max_msg_quit_count; +static int disable_pipelining; +static int disable_8bitmime; +static int disable_esmtp; +static int enable_lmtp; +static int pretend_pix; +static int disable_saslauth; +static int disable_xclient; +static int disable_xforward; +static int disable_enh_status; +static int disable_dsn; +static int max_client_count = DEF_MAX_CLIENT_COUNT; +static int client_count; +static int sock; +static int abort_delay = -1; +static int data_read_delay = 0; + +static char *single_template; /* individual template */ +static char *shared_template; /* shared template */ +static VSTRING *start_string; /* dump content prefix */ + +static const INET_PROTO_INFO *proto_info; + +#define STR(x) vstring_str(x) + +/* do_stats - show counters */ + +static void do_stats(void) +{ + vstream_printf("sess=%d quit=%d mesg=%d\r", + sess_count, quit_count, mesg_count); + vstream_fflush(VSTREAM_OUT); +} + +/* hard_err_resp - generic hard error response */ + +static void hard_err_resp(SINK_STATE *state) +{ + smtp_printf(state->stream, "%s", hard_error_resp); + SMTP_FLUSH(state->stream); +} + +/* soft_err_resp - generic soft error response */ + +static void soft_err_resp(SINK_STATE *state) +{ + smtp_printf(state->stream, "%s", soft_error_resp); + SMTP_FLUSH(state->stream); +} + +/* exp_path_template - expand template pathname, static result */ + +static VSTRING *exp_path_template(const char *template, time_t start_time) +{ + static VSTRING *path_buf = 0; + struct tm *lt; + + if (path_buf == 0) + path_buf = vstring_alloc(100); + else + VSTRING_RESET(path_buf); + lt = localtime(&start_time); + while (strftime(STR(path_buf), vstring_avail(path_buf), template, lt) == 0) + VSTRING_SPACE(path_buf, vstring_avail(path_buf) + 100); + VSTRING_SKIP(path_buf); + return (path_buf); +} + +/* make_parent_dir - create parent directory or bust */ + +static void make_parent_dir(const char *path, mode_t mode) +{ + const char *parent; + + parent = sane_dirname((VSTRING *) 0, path); + if (make_dirs(parent, mode) < 0) + msg_fatal("mkdir %s: %m", parent); +} + +/* mail_file_open - open mail capture file */ + +static void mail_file_open(SINK_STATE *state) +{ + const char *myname = "mail_file_open"; + VSTRING *path_buf; + ssize_t len; + int tries = 0; + + /* + * Save the start time for later. + */ + time(&(state->start_time)); + + /* + * Expand the per-message dumpfile pathname template. + */ + path_buf = exp_path_template(single_template, state->start_time); + + /* + * Append a random hexadecimal string to the pathname and create a new + * file. Retry with a different path if the file already exists. Create + * intermediate directories on the fly when the template specifies + * multiple pathname segments. + */ +#define ID_FORMAT "%08x" + + for (len = VSTRING_LEN(path_buf); /* void */ ; vstring_truncate(path_buf, len)) { + if (++tries > 100) + msg_fatal("%s: something is looping", myname); + state->id = myrand(); + vstring_sprintf_append(path_buf, ID_FORMAT, state->id); + if ((state->dump_file = vstream_fopen(STR(path_buf), + O_RDWR | O_CREAT | O_EXCL, + 0644)) != 0) { + break; + } else if (errno == EEXIST) { + continue; + } else if (errno == ENOENT) { + make_parent_dir(STR(path_buf), 0755); + continue; + } else { + msg_fatal("open %s: %m", STR(path_buf)); + } + } + + /* + * Don't leave temporary files behind. + */ + if (shared_template != 0 && unlink(STR(path_buf)) < 0) + msg_fatal("unlink %s: %m", STR(path_buf)); + + /* + * Do initial header records. + */ + if (start_string) + vstream_fprintf(state->dump_file, "%s", STR(start_string)); + vstream_fprintf(state->dump_file, "X-Client-Addr: %s%s\n", + state->addr_prefix, state->client_addr.buf); + vstream_fprintf(state->dump_file, "X-Client-Proto: %s\n", state->client_proto); + if (state->helo_args) + vstream_fprintf(state->dump_file, "X-Helo-Args: %s\n", state->helo_args); + /* Note: there may be more than one recipient. */ +} + +/* mail_file_finish_header - do final smtp-sink generated header records */ + +static void mail_file_finish_header(SINK_STATE *state) +{ + if (state->helo_args) + vstream_fprintf(state->dump_file, "Received: from %s ([%s%s])\n", + state->helo_args, state->addr_prefix, + state->client_addr.buf); + else + vstream_fprintf(state->dump_file, "Received: from [%s%s] ([%s%s])\n", + state->addr_prefix, state->client_addr.buf, + state->addr_prefix, state->client_addr.buf); + vstream_fprintf(state->dump_file, "\tby %s (smtp-sink)" + " with %s id " ID_FORMAT ";\n", + var_myhostname, state->client_proto, state->id); + vstream_fprintf(state->dump_file, "\t%s\n", mail_date(state->start_time)); +} + +/* mail_file_cleanup - common cleanup for capture file */ + +static void mail_file_cleanup(SINK_STATE *state) +{ + (void) vstream_fclose(state->dump_file); + state->dump_file = 0; +} + +/* mail_file_finish - handle message completion for capture file */ + +static void mail_file_finish(SINK_STATE *state) +{ + + /* + * Optionally append the captured message to a shared dumpfile. + */ + if (shared_template) { + const char *out_path; + VSTREAM *out_fp; + ssize_t count; + + /* + * Expand the shared dumpfile pathname template. + */ + out_path = STR(exp_path_template(shared_template, state->start_time)); + + /* + * Open the shared dump file. + */ +#define OUT_OPEN_FLAGS (O_WRONLY | O_CREAT | O_APPEND) +#define OUT_OPEN_MODE 0644 + + if ((out_fp = vstream_fopen(out_path, OUT_OPEN_FLAGS, OUT_OPEN_MODE)) + == 0 && errno == ENOENT) { + make_parent_dir(out_path, 0755); + out_fp = vstream_fopen(out_path, OUT_OPEN_FLAGS, OUT_OPEN_MODE); + } + if (out_fp == 0) + msg_fatal("open %s: %m", out_path); + + /* + * Append message content from single-message dump file. + */ + if (vstream_fseek(state->dump_file, 0L, SEEK_SET) < 0) + msg_fatal("seek file %s: %m", VSTREAM_PATH(state->dump_file)); + VSTRING_RESET(state->buffer); + for (;;) { + count = vstream_fread(state->dump_file, STR(state->buffer), + vstring_avail(state->buffer)); + if (count <= 0) + break; + if (vstream_fwrite(out_fp, STR(state->buffer), count) != count) + msg_fatal("append file %s: %m", out_path); + } + if (vstream_ferror(state->dump_file)) + msg_fatal("read file %s: %m", VSTREAM_PATH(state->dump_file)); + if (vstream_fclose(out_fp)) + msg_fatal("append file %s: %m", out_path); + } + mail_file_cleanup(state); +} + +/* mail_file_reset - abort mail to capture file */ + +static void mail_file_reset(SINK_STATE *state) +{ + if (shared_template == 0 + && unlink(VSTREAM_PATH(state->dump_file)) < 0 + && errno != ENOENT) + msg_fatal("unlink %s: %m", VSTREAM_PATH(state->dump_file)); + mail_file_cleanup(state); +} + +/* mail_cmd_reset - reset mail transaction information */ + +static void mail_cmd_reset(SINK_STATE *state) +{ + state->in_mail = 0; + /* Not: state->rcpts = 0. This breaks the DOT reply with LMTP. */ + if (state->dump_file) + mail_file_reset(state); +} + +/* ehlo_response - respond to EHLO command */ + +static void ehlo_response(SINK_STATE *state, const char *args) +{ +#define SKIP(cp, cond) do { \ + for (/* void */; *cp && (cond); cp++) \ + /* void */; \ + } while (0) + + /* EHLO aborts a mail transaction in progress. */ + mail_cmd_reset(state); + if (enable_lmtp == 0) + state->client_proto = "ESMTP"; + smtp_printf(state->stream, "250-%s", var_myhostname); + if (!disable_pipelining) + smtp_printf(state->stream, "250-PIPELINING"); + if (!disable_8bitmime) + smtp_printf(state->stream, "250-8BITMIME"); + if (!disable_saslauth) + smtp_printf(state->stream, "250-AUTH PLAIN LOGIN"); + if (!disable_xclient) + smtp_printf(state->stream, "250-XCLIENT NAME HELO"); + if (!disable_xforward) + smtp_printf(state->stream, "250-XFORWARD NAME ADDR PROTO HELO"); + if (!disable_enh_status) + smtp_printf(state->stream, "250-ENHANCEDSTATUSCODES"); + if (!disable_dsn) + smtp_printf(state->stream, "250-DSN"); + /* RFC 821/2821/5321: Format is replycode<SPACE>optional-text<CRLF> */ + smtp_printf(state->stream, "250 "); + SMTP_FLUSH(state->stream); + if (single_template) { + if (state->helo_args) + myfree(state->helo_args); + SKIP(args, ISSPACE(*args)); + state->helo_args = mystrdup(args); + } +} + +/* helo_response - respond to HELO command */ + +static void helo_response(SINK_STATE *state, const char *args) +{ + /* HELO aborts a mail transaction in progress. */ + mail_cmd_reset(state); + state->client_proto = "SMTP"; + smtp_printf(state->stream, "250 %s", var_myhostname); + SMTP_FLUSH(state->stream); + if (single_template) { + if (state->helo_args) + myfree(state->helo_args); + SKIP(args, ISSPACE(*args)); + state->helo_args = mystrdup(args); + } +} + +/* ok_response - send 250 OK */ + +static void ok_response(SINK_STATE *state, const char *unused_args) +{ + smtp_printf(state->stream, "250 2.0.0 Ok"); + SMTP_FLUSH(state->stream); +} + +/* rset_response - reset, send 250 OK */ + +static void rset_response(SINK_STATE *state, const char *unused_args) +{ + mail_cmd_reset(state); + smtp_printf(state->stream, "250 2.1.0 Ok"); + SMTP_FLUSH(state->stream); +} + +/* mail_response - reset recipient count, send 250 OK */ + +static void mail_response(SINK_STATE *state, const char *args) +{ + if (state->in_mail) { + smtp_printf(state->stream, "503 5.5.1 Error: nested MAIL command"); + SMTP_FLUSH(state->stream); + return; + } + state->in_mail++; + state->rcpts = 0; + smtp_printf(state->stream, "250 2.1.0 Ok"); + SMTP_FLUSH(state->stream); + if (single_template) { + mail_file_open(state); + SKIP(args, *args != ':'); + SKIP(args, *args == ':'); + SKIP(args, ISSPACE(*args)); + vstream_fprintf(state->dump_file, "X-Mail-Args: %s\n", args); + } +} + +/* rcpt_response - bump recipient count, send 250 OK */ + +static void rcpt_response(SINK_STATE *state, const char *args) +{ + if (state->in_mail == 0) { + smtp_printf(state->stream, "503 5.5.1 Error: need MAIL command"); + SMTP_FLUSH(state->stream); + return; + } + state->rcpts++; + smtp_printf(state->stream, "250 2.1.5 Ok"); + SMTP_FLUSH(state->stream); + /* Note: there may be more than one recipient per mail transaction. */ + if (state->dump_file) { + SKIP(args, *args != ':'); + SKIP(args, *args == ':'); + SKIP(args, ISSPACE(*args)); + vstream_fprintf(state->dump_file, "X-Rcpt-Args: %s\n", args); + } +} + +/* abort_event - delayed abort after DATA command */ + +static void abort_event(int unused_event, void *context) +{ + SINK_STATE *state = (SINK_STATE *) context; + + smtp_printf(state->stream, "550 This violates SMTP"); + SMTP_FLUSH(state->stream); + disconnect(state); +} + +/* delay_read_event - resume input event handling */ + +static void delay_read_event(int event, void *context) +{ + SINK_STATE *state = (SINK_STATE *) context; + + if (event != EVENT_TIME) + msg_panic("delay_read_event: non-timer event %d", event); + + event_enable_read(vstream_fileno(state->stream), read_event, (void *) state); + event_request_timer(read_timeout, (void *) state, var_tmout); +} + +/* delay_read - temporarily suspend input event handling */ + +static void delay_read(SINK_STATE *state, int delay) +{ + event_disable_readwrite(vstream_fileno(state->stream)); + event_cancel_timer(read_timeout, (void *) state); + event_request_timer(delay_read_event, (void *) state, delay); +} + +/* data_response - respond to DATA command */ + +static void data_response(SINK_STATE *state, const char *unused_args) +{ + if (state->in_mail == 0 || state->rcpts == 0) { + smtp_printf(state->stream, "503 5.5.1 Error: need RCPT command"); + SMTP_FLUSH(state->stream); + return; + } + /* Not: ST_ANY. */ + state->data_state = ST_CR_LF; + smtp_printf(state->stream, "354 End data with <CR><LF>.<CR><LF>"); + SMTP_FLUSH(state->stream); + if (abort_delay < 0) { + state->read_fn = data_read; + /* Todo: move into code that invokes the command response function. */ + if (data_read_delay > 0) + delay_read(state, data_read_delay); + } else { + /* Stop reading, send premature 550, and disconnect. */ + event_disable_readwrite(vstream_fileno(state->stream)); + event_cancel_timer(read_event, (void *) state); + event_request_timer(abort_event, (void *) state, abort_delay); + } + if (state->dump_file) + mail_file_finish_header(state); +} + +/* dot_resp_hard - hard error response to . command */ + +static void dot_resp_hard(SINK_STATE *state) +{ + if (enable_lmtp) { + while (state->rcpts-- > 0) /* XXX this could block */ + smtp_printf(state->stream, "%s", hard_error_resp); + } else { + smtp_printf(state->stream, "%s", hard_error_resp); + } + SMTP_FLUSH(state->stream); +} + +/* dot_resp_soft - soft error response to . command */ + +static void dot_resp_soft(SINK_STATE *state) +{ + if (enable_lmtp) { + while (state->rcpts-- > 0) /* XXX this could block */ + smtp_printf(state->stream, "%s", soft_error_resp); + } else { + smtp_printf(state->stream, "%s", soft_error_resp); + } + SMTP_FLUSH(state->stream); +} + +/* dot_response - response to . command */ + +static void dot_response(SINK_STATE *state, const char *unused_args) +{ + if (enable_lmtp) { + while (state->rcpts-- > 0) /* XXX this could block */ + smtp_printf(state->stream, "250 2.2.0 Ok"); + } else { + smtp_printf(state->stream, "250 2.0.0 Ok"); + } + SMTP_FLUSH(state->stream); +} + +/* quit_response - respond to QUIT command */ + +static void quit_response(SINK_STATE *state, const char *unused_args) +{ + smtp_printf(state->stream, "221 Bye"); + smtp_flush(state->stream); /* not: SMTP_FLUSH */ + if (show_count) + quit_count++; +} + +/* conn_response - respond to connect command */ + +static void conn_response(SINK_STATE *state, const char *unused_args) +{ + if (pretend_pix) + smtp_printf(state->stream, "220 ********"); + else if (disable_esmtp) + smtp_printf(state->stream, "220 %s", var_myhostname); + else + smtp_printf(state->stream, "220 %s ESMTP", var_myhostname); + SMTP_FLUSH(state->stream); +} + +/* delay_event - delayed command response */ + +static void delay_event(int unused_event, void *context) +{ + SINK_STATE *state = (SINK_STATE *) context; + + switch (vstream_setjmp(state->stream)) { + + default: + msg_panic("unknown read/write error"); + /* NOTREACHED */ + + case SMTP_ERR_TIME: + msg_warn("write timeout"); + disconnect(state); + return; + + case SMTP_ERR_EOF: + msg_warn("lost connection"); + disconnect(state); + return; + + case 0: + state->delayed_response(state, state->delayed_args); + myfree(state->delayed_args); + state->delayed_args = 0; + break; + } + + if (state->delayed_response == quit_response) { + disconnect(state); + return; + } + state->delayed_response = 0; + + /* Resume input event handling after the delayed response. */ + event_enable_read(vstream_fileno(state->stream), read_event, (void *) state); + event_request_timer(read_timeout, (void *) state, var_tmout); +} + +/* data_read - read data from socket */ + +static int data_read(SINK_STATE *state) +{ + int ch; + struct data_trans { + int state; + int want; + int next_state; + }; + static struct data_trans data_trans[] = { + ST_ANY, '\r', ST_CR, + ST_CR, '\n', ST_CR_LF, + ST_CR_LF, '.', ST_CR_LF_DOT, + ST_CR_LF_DOT, '\r', ST_CR_LF_DOT_CR, + ST_CR_LF_DOT_CR, '\n', ST_CR_LF_DOT_CR_LF, + }; + struct data_trans *dp; + + /* + * A read may result in EOF, but is never supposed to time out - a time + * out means that we were trying to read when no data was available. + */ + for (;;) { + if ((ch = VSTREAM_GETC(state->stream)) == VSTREAM_EOF) + return (-1); + for (dp = data_trans; dp->state != state->data_state; dp++) + /* void */ ; + + /* + * Try to match the current character desired by the state machine. + * If that fails, try to restart the machine with a match for its + * first state. This covers the case of a CR/LF/CR/LF sequence + * (empty line) right before the end of the message data. + */ + if (ch == dp->want) + state->data_state = dp->next_state; + else if (ch == data_trans[0].want) + state->data_state = data_trans[0].next_state; + else + state->data_state = ST_ANY; + if (state->dump_file) { + if (ch != '\r' && state->data_state != ST_CR_LF_DOT) + VSTREAM_PUTC(ch, state->dump_file); + if (vstream_ferror(state->dump_file)) + msg_fatal("append file %s: %m", VSTREAM_PATH(state->dump_file)); + } + if (state->data_state == ST_CR_LF_DOT_CR_LF) { + PUSH_BACK_SET(state, ".\r\n"); + state->read_fn = command_read; + state->data_state = ST_ANY; + if (state->dump_file) + mail_file_finish(state); + mail_cmd_reset(state); + if (show_count || max_msg_quit_count > 0) { + mesg_count++; + if (show_count) + do_stats(); + if (max_msg_quit_count > 0 && mesg_count >= max_msg_quit_count) + exit(0); + } + break; + } + + /* + * We must avoid blocking I/O, so get out of here as soon as both the + * VSTREAM and kernel read buffers dry up. + */ + if (vstream_peek(state->stream) <= 0 + && readable(vstream_fileno(state->stream)) <= 0) + return (0); + } + return (0); +} + + /* + * The table of all SMTP commands that we can handle. + */ +typedef struct SINK_COMMAND { + const char *name; + void (*response) (SINK_STATE *, const char *); + void (*hard_response) (SINK_STATE *); + void (*soft_response) (SINK_STATE *); + int flags; + int delay; + int delay_odds; +} SINK_COMMAND; + +#define FLAG_ENABLE (1<<0) /* command is enabled */ +#define FLAG_SYSLOG (1<<1) /* log the command */ +#define FLAG_HARD_ERR (1<<2) /* report hard error */ +#define FLAG_SOFT_ERR (1<<3) /* report soft error */ +#define FLAG_DISCONNECT (1<<4) /* disconnect */ +#define FLAG_CLOSE (1<<5) /* say goodbye and disconnect */ + +static SINK_COMMAND command_table[] = { + "connect", conn_response, hard_err_resp, soft_err_resp, 0, 0, 0, + "helo", helo_response, hard_err_resp, soft_err_resp, 0, 0, 0, + "ehlo", ehlo_response, hard_err_resp, soft_err_resp, 0, 0, 0, + "lhlo", ehlo_response, hard_err_resp, soft_err_resp, 0, 0, 0, + "xclient", ok_response, hard_err_resp, soft_err_resp, FLAG_ENABLE, 0, 0, + "xforward", ok_response, hard_err_resp, soft_err_resp, FLAG_ENABLE, 0, 0, + "auth", ok_response, hard_err_resp, soft_err_resp, FLAG_ENABLE, 0, 0, + "mail", mail_response, hard_err_resp, soft_err_resp, FLAG_ENABLE, 0, 0, + "rcpt", rcpt_response, hard_err_resp, soft_err_resp, FLAG_ENABLE, 0, 0, + "data", data_response, hard_err_resp, soft_err_resp, FLAG_ENABLE, 0, 0, + ".", dot_response, dot_resp_hard, dot_resp_soft, FLAG_ENABLE, 0, 0, + "rset", rset_response, hard_err_resp, soft_err_resp, FLAG_ENABLE, 0, 0, + "noop", ok_response, hard_err_resp, soft_err_resp, FLAG_ENABLE, 0, 0, + "vrfy", ok_response, hard_err_resp, soft_err_resp, FLAG_ENABLE, 0, 0, + "quit", quit_response, hard_err_resp, soft_err_resp, FLAG_ENABLE, 0, 0, + 0, +}; + +/* reset_cmd_flags - reset per-command command flags */ + +static void reset_cmd_flags(const char *cmd, int flags) +{ + SINK_COMMAND *cmdp; + + for (cmdp = command_table; cmdp->name != 0; cmdp++) + if (strcasecmp(cmd, cmdp->name) == 0) + break; + if (cmdp->name == 0) + msg_fatal("unknown command: %s", cmd); + cmdp->flags &= ~flags; +} + +/* set_cmd_flags - set per-command command flags */ + +static void set_cmd_flags(const char *cmd, int flags) +{ + SINK_COMMAND *cmdp; + + for (cmdp = command_table; cmdp->name != 0; cmdp++) + if (strcasecmp(cmd, cmdp->name) == 0) + break; + if (cmdp->name == 0) + msg_fatal("unknown command: %s", cmd); + cmdp->flags |= flags; +} + +/* set_cmds_flags - set per-command flags for multiple commands */ + +static void set_cmds_flags(const char *cmds, int flags) +{ + char *saved_cmds; + char *cp; + char *cmd; + + saved_cmds = cp = mystrdup(cmds); + while ((cmd = mystrtok(&cp, CHARS_COMMA_SP)) != 0) + set_cmd_flags(cmd, flags); + myfree(saved_cmds); +} + +/* set_cmd_delay - set per-command delay */ + +static void set_cmd_delay(const char *cmd, int delay, int odds) +{ + SINK_COMMAND *cmdp; + + for (cmdp = command_table; cmdp->name != 0; cmdp++) + if (strcasecmp(cmd, cmdp->name) == 0) + break; + if (cmdp->name == 0) + msg_fatal("unknown command: %s", cmd); + + if (delay <= 0) + msg_fatal("non-positive '%s' delay", cmd); + if (odds < 0 || odds > 99) + msg_fatal("delay odds for '%s' out of range", cmd); + + cmdp->delay = delay; + cmdp->delay_odds = odds; +} + +/* set_cmd_delay_arg - set per-command delay from option argument */ + +static void set_cmd_delay_arg(char *arg) +{ + char *cp; + char *saved_arg; + char *cmd; + char *delay; + char *odds; + + saved_arg = cp = mystrdup(arg); + cmd = mystrtok(&cp, ":"); + delay = mystrtok(&cp, ":"); + if (cmd == 0 || delay == 0) + msg_fatal("invalid command delay argument: %s", arg); + odds = mystrtok(&cp, ""); + set_cmd_delay(cmd, atoi(delay), odds ? atoi(odds) : 0); + myfree(saved_arg); +} + +/* command_resp - respond to command */ + +static int command_resp(SINK_STATE *state, SINK_COMMAND *cmdp, + const char *command, const char *args) +{ + /* We use raw syslog. Sanitize data content and length. */ + if (cmdp->flags & FLAG_SYSLOG) + syslog(LOG_INFO, "%s %.100s", command, args); + if (cmdp->flags & FLAG_DISCONNECT) + return (-1); + if (cmdp->flags & FLAG_CLOSE) { + smtp_printf(state->stream, "421 4.0.0 Server closing connection"); + return (-1); + } + if (cmdp->flags & FLAG_HARD_ERR) { + cmdp->hard_response(state); + return (0); + } + if (cmdp->flags & FLAG_SOFT_ERR) { + cmdp->soft_response(state); + return (0); + } + if (cmdp->delay > 0) { + int delay = cmdp->delay; + + if (cmdp->delay_odds > 0) + for (delay = 0; + ((int) (100.0 * rand() / (RAND_MAX + 1.0))) < cmdp->delay_odds; + delay += cmdp->delay) + /* NOP */ ; + /* Suspend input event handling while delaying the command response. */ + event_disable_readwrite(vstream_fileno(state->stream)); + event_cancel_timer(read_timeout, (void *) state); + event_request_timer(delay_event, (void *) state, delay); + state->delayed_response = cmdp->response; + state->delayed_args = mystrdup(args); + } else { + cmdp->response(state, args); + if (cmdp->response == quit_response) + return (-1); + } + return (0); +} + +/* command_read - talk the SMTP protocol, server side */ + +static int command_read(SINK_STATE *state) +{ + char *command; + SINK_COMMAND *cmdp; + int ch; + struct cmd_trans { + int state; + int want; + int next_state; + }; + static struct cmd_trans cmd_trans[] = { + ST_ANY, '\r', ST_CR, + ST_CR, '\n', ST_CR_LF, + 0, 0, 0, + }; + struct cmd_trans *cp; + char *ptr; + + /* + * A read may result in EOF, but is never supposed to time out - a time + * out means that we were trying to read when no data was available. + */ +#define NEXT_CHAR(state) \ + (PUSH_BACK_PEEK(state) ? PUSH_BACK_GET(state) : VSTREAM_GETC(state->stream)) + + if (state->data_state == ST_CR_LF) + state->data_state = ST_ANY; /* XXX */ + for (;;) { + if ((ch = NEXT_CHAR(state)) == VSTREAM_EOF) + return (-1); + + /* + * Sanity check. We don't want to store infinitely long commands. + */ + if (VSTRING_LEN(state->buffer) >= var_max_line_length) { + msg_warn("command line too long"); + return (-1); + } + VSTRING_ADDCH(state->buffer, ch); + + /* + * Try to match the current character desired by the state machine. + * If that fails, try to restart the machine with a match for its + * first state. + */ + for (cp = cmd_trans; cp->state != state->data_state; cp++) + if (cp->want == 0) + msg_panic("command_read: unknown state: %d", state->data_state); + if (ch == cp->want) + state->data_state = cp->next_state; + else if (ch == cmd_trans[0].want) + state->data_state = cmd_trans[0].next_state; + else + state->data_state = ST_ANY; + if (state->data_state == ST_CR_LF) + break; + + /* + * We must avoid blocking I/O, so get out of here as soon as both the + * VSTREAM and kernel read buffers dry up. + * + * XXX Solaris non-blocking read() may fail on a socket when ioctl + * FIONREAD reports there is unread data. Diagnosis by Max Pashkov. + * As a workaround we use readable() (which uses poll or select()) + * instead of peek_fd() (which uses ioctl FIONREAD). Workaround added + * 20020604. + */ + if (PUSH_BACK_PEEK(state) == 0 && vstream_peek(state->stream) <= 0 + && readable(vstream_fileno(state->stream)) <= 0) + return (0); + } + + /* + * Properly terminate the result, and reset the buffer write pointer for + * reading the next command. This is ugly, but not as ugly as trying to + * deal with all the early returns below. + */ + vstring_truncate(state->buffer, VSTRING_LEN(state->buffer) - 2); + VSTRING_TERMINATE(state->buffer); + state->data_state = ST_CR_LF; + VSTRING_RESET(state->buffer); + + /* + * Got a complete command line. Parse it. + */ + ptr = vstring_str(state->buffer); + if (msg_verbose) + msg_info("%s", ptr); + if ((command = mystrtok(&ptr, " \t")) == 0) { + smtp_printf(state->stream, "500 5.5.2 Error: unknown command"); + SMTP_FLUSH(state->stream); + return (0); + } + for (cmdp = command_table; cmdp->name != 0; cmdp++) + if (strcasecmp(command, cmdp->name) == 0) + break; + if (cmdp->name == 0 || (cmdp->flags & FLAG_ENABLE) == 0) { + smtp_printf(state->stream, "500 5.5.1 Error: unknown command"); + SMTP_FLUSH(state->stream); + return (0); + } + return (command_resp(state, cmdp, command, printable(ptr, '?'))); +} + +/* read_timeout - handle timer event */ + +static void read_timeout(int unused_event, void *context) +{ + SINK_STATE *state = (SINK_STATE *) context; + + /* + * We don't send anything to the client, because we would have to set up + * an smtp_stream exception handler first. And that is just too much + * trouble. + */ + msg_warn("read timeout"); + disconnect(state); +} + +/* read_event - handle command or data read events */ + +static void read_event(int unused_event, void *context) +{ + SINK_STATE *state = (SINK_STATE *) context; + + /* + * The input reading routine not only reads input (with vstream calls) + * but also produces output (with smtp_stream calls). Because the output + * routines can raise timeout or EOF exceptions with vstream_longjmp(), + * the input reading routine needs to set up corresponding exception + * handlers with vstream_setjmp(). Guarding the input operations in the + * same manner is not useful: we must read input in non-blocking mode, so + * we never get called when the socket stays unreadable too long. And EOF + * is already trivial to detect with the vstream calls. + */ + do { + switch (vstream_setjmp(state->stream)) { + + default: + msg_panic("unknown read/write error"); + /* NOTREACHED */ + + case SMTP_ERR_TIME: + msg_warn("write timeout"); + disconnect(state); + return; + + case SMTP_ERR_EOF: + msg_warn("lost connection"); + disconnect(state); + return; + + case 0: + if (state->read_fn(state) < 0) { + if (msg_verbose) + msg_info("disconnect"); + disconnect(state); + return; + } + } + } while (PUSH_BACK_PEEK(state) != 0 || vstream_peek(state->stream) > 0); + + /* + * Reset the idle timer. Wait until the next input event, or until the + * idle timer goes off. + */ + event_request_timer(read_timeout, (void *) state, var_tmout); +} + +static void connect_event(int, void *); + +/* disconnect - handle disconnection events */ + +static void disconnect(SINK_STATE *state) +{ + event_disable_readwrite(vstream_fileno(state->stream)); + event_cancel_timer(read_timeout, (void *) state); + if (show_count) { + sess_count++; + do_stats(); + } + vstream_fclose(state->stream); + vstring_free(state->buffer); + /* Clean up file capture attributes. */ + if (state->helo_args) + myfree(state->helo_args); + /* Delete incomplete mail transaction. */ + mail_cmd_reset(state); + if (state->delayed_args) + myfree(state->delayed_args); + myfree((void *) state); + if (max_quit_count > 0 && quit_count >= max_quit_count) + exit(0); + if (client_count-- == max_client_count) + event_enable_read(sock, connect_event, (void *) 0); +} + +/* connect_event - handle connection events */ + +static void connect_event(int unused_event, void *unused_context) +{ + struct sockaddr_storage ss; + SOCKADDR_SIZE len = sizeof(ss); + struct sockaddr *sa = (struct sockaddr *) &ss; + SINK_STATE *state; + int fd; + + if ((fd = sane_accept(sock, sa, &len)) >= 0) { + /* Safety: limit the number of open sockets and capture files. */ + if (++client_count == max_client_count) + event_disable_readwrite(sock); + state = (SINK_STATE *) mymalloc(sizeof(*state)); + if (strchr((char *) proto_info->sa_family_list, sa->sa_family)) + SOCKADDR_TO_HOSTADDR(sa, len, &state->client_addr, + (MAI_SERVPORT_STR *) 0, sa->sa_family); + else + strncpy(state->client_addr.buf, "local", sizeof("local") + 0); + if (msg_verbose) + msg_info("connect (%s %s)", +#ifdef AF_LOCAL + sa->sa_family == AF_LOCAL ? "AF_LOCAL" : +#else + sa->sa_family == AF_UNIX ? "AF_UNIX" : +#endif + sa->sa_family == AF_INET ? "AF_INET" : +#ifdef AF_INET6 + sa->sa_family == AF_INET6 ? "AF_INET6" : +#endif + "unknown protocol family", + state->client_addr.buf); + non_blocking(fd, NON_BLOCKING); + state->stream = vstream_fdopen(fd, O_RDWR); + vstream_tweak_sock(state->stream); + state->buffer = vstring_alloc(1024); + state->read_fn = command_read; + state->data_state = ST_ANY; + PUSH_BACK_SET(state, ""); + smtp_timeout_setup(state->stream, var_tmout); + state->in_mail = 0; + state->rcpts = 0; + state->delayed_response = 0; + state->delayed_args = 0; + /* Initialize file capture attributes. */ +#ifdef AF_INET6 + if (sa->sa_family == AF_INET6) + state->addr_prefix = "ipv6:"; + else +#endif + state->addr_prefix = ""; + + state->helo_args = 0; + state->client_proto = enable_lmtp ? "LMTP" : "SMTP"; + state->start_time = 0; + state->id = 0; + state->dump_file = 0; + + /* + * We use the smtp_stream module to produce output. That module + * throws an exception via vstream_longjmp() in case of a timeout or + * lost connection error. Therefore we must prepare to handle these + * exceptions with vstream_setjmp(). + */ + switch (vstream_setjmp(state->stream)) { + + default: + msg_panic("unknown read/write error"); + /* NOTREACHED */ + + case SMTP_ERR_TIME: + msg_warn("write timeout"); + disconnect(state); + return; + + case SMTP_ERR_EOF: + msg_warn("lost connection"); + disconnect(state); + return; + + case 0: + if (command_resp(state, command_table, "connect", "") < 0) + disconnect(state); + else if (command_table->delay == 0) { + event_enable_read(fd, read_event, (void *) state); + event_request_timer(read_timeout, (void *) state, var_tmout); + } + } + } +} + +/* usage - explain */ + +static void usage(char *myname) +{ + msg_fatal("usage: %s [-468acCeEFLpPv] [-A abort_delay] [-b soft_bounce_reply] [-B hard_bounce_reply] [-d dump-template] [-D dump-template] [-f commands] [-h hostname] [-m max_concurrency] [-M message_quit_count] [-n quit_count] [-q commands] [-r commands] [-R root-dir] [-s commands] [-S start-string] [-u user_privs] [-w delay] [host]:port backlog", myname); +} + +MAIL_VERSION_STAMP_DECLARE; + +int main(int argc, char **argv) +{ + int backlog; + int ch; + int delay; + const char *protocols = INET_PROTO_NAME_ALL; + const char *root_dir = 0; + const char *user_privs = 0; + + /* + * Fingerprint executables and core dumps. + */ + MAIL_VERSION_STAMP_ALLOCATE; + + /* + * Fix 20051207. + */ + signal(SIGPIPE, SIG_IGN); + + /* + * Initialize diagnostics. + */ + msg_vstream_init(argv[0], VSTREAM_ERR); + + /* + * Parse JCL. + */ + while ((ch = GETOPT(argc, argv, "468aA:b:B:cCd:D:eEf:Fh:H:Ln:m:M:NpPq:Q:r:R:s:S:t:T:u:vw:W:")) > 0) { + switch (ch) { + case '4': + protocols = INET_PROTO_NAME_IPV4; + break; + case '6': + protocols = INET_PROTO_NAME_IPV6; + break; + case '8': + disable_8bitmime = 1; + break; + case 'a': + disable_saslauth = 1; + break; + case 'A': + if (!alldig(optarg) || (abort_delay = atoi(optarg)) < 0) + usage(argv[0]); + break; + case 'b': + if (optarg[0] != '4' || strspn(optarg, "0123456789") != 3) { + msg_error("bad soft error reply: %s", optarg); + usage(argv[0]); + } else + soft_error_resp = optarg; + break; + case 'B': + if (optarg[0] != '5' || strspn(optarg, "0123456789") != 3) { + msg_error("bad hard error reply: %s", optarg); + usage(argv[0]); + } else + hard_error_resp = optarg; + break; + case 'c': + show_count++; + break; + case 'C': + disable_xclient = 1; + reset_cmd_flags("xclient", FLAG_ENABLE); + break; + case 'd': + single_template = optarg; + break; + case 'D': + shared_template = optarg; + break; + case 'e': + disable_esmtp = 1; + break; + case 'E': + disable_enh_status = 1; + break; + case 'f': + set_cmds_flags(optarg, FLAG_HARD_ERR); + disable_pipelining = 1; + break; + case 'F': + disable_xforward = 1; + reset_cmd_flags("xforward", FLAG_ENABLE); + break; + case 'h': + var_myhostname = optarg; + break; + case 'H': + if ((data_read_delay = atoi(optarg)) <= 0) + msg_fatal("bad data read delay: %s", optarg); + break; + case 'L': + enable_lmtp = 1; + break; + case 'm': + if ((max_client_count = atoi(optarg)) <= 0) + msg_fatal("bad concurrency limit: %s", optarg); + break; + case 'M': + if ((max_msg_quit_count = atoi(optarg)) <= 0) + msg_fatal("bad message quit count: %s", optarg); + break; + case 'n': + if ((max_quit_count = atoi(optarg)) <= 0) + msg_fatal("bad quit count: %s", optarg); + break; + case 'N': + disable_dsn = 1; + break; + case 'p': + disable_pipelining = 1; + break; + case 'P': + pretend_pix = 1; + disable_esmtp = 1; + break; + case 'q': + set_cmds_flags(optarg, FLAG_DISCONNECT); + break; + case 'Q': + set_cmds_flags(optarg, FLAG_CLOSE); + break; + case 'r': + set_cmds_flags(optarg, FLAG_SOFT_ERR); + disable_pipelining = 1; + break; + case 'R': + root_dir = optarg; + break; + case 's': + openlog(basename(argv[0]), LOG_PID, LOG_MAIL); + set_cmds_flags(optarg, FLAG_SYSLOG); + break; + case 'S': + start_string = vstring_alloc(10); + unescape(start_string, optarg); + break; + case 't': + if ((var_tmout = atoi(optarg)) <= 0) + msg_fatal("bad timeout: %s", optarg); + break; + case 'T': + if ((inet_windowsize = atoi(optarg)) <= 0) + msg_fatal("bad TCP window size: %s", optarg); + break; + case 'u': + user_privs = optarg; + break; + case 'v': + msg_verbose++; + break; + case 'w': + if ((delay = atoi(optarg)) <= 0) + usage(argv[0]); + set_cmd_delay("data", delay, 0); + break; + case 'W': + set_cmd_delay_arg(optarg); + break; + default: + usage(argv[0]); + } + } + if (argc - optind != 2) + usage(argv[0]); + if ((backlog = atoi(argv[optind + 1])) <= 0) + usage(argv[0]); + if (single_template && shared_template) + msg_fatal("use only one of -d or -D, but not both"); + if (geteuid() == 0 && user_privs == 0) + msg_fatal("-u option is required if running as root"); + + /* + * Initialize. + */ + if (var_myhostname == 0) + var_myhostname = "smtp-sink"; + set_cmds_flags(enable_lmtp ? "lhlo" : + disable_esmtp ? "helo" : + "helo, ehlo", FLAG_ENABLE); + proto_info = inet_proto_init("protocols", protocols); + if (strncmp(argv[optind], "unix:", 5) == 0) { + sock = unix_listen(argv[optind] + 5, backlog, BLOCKING); + } else { + if (strncmp(argv[optind], "inet:", 5) == 0) + argv[optind] += 5; + sock = inet_listen(argv[optind], backlog, BLOCKING); + } + if (user_privs) + chroot_uid(root_dir, user_privs); + + if (single_template) + mysrand((int) time((time_t *) 0)); + else if (shared_template) + single_template = shared_template; + + /* + * Start the event handler. + */ + event_enable_read(sock, connect_event, (void *) 0); + for (;;) + event_loop(-1); +} |