/*++ /* NAME /* pipe 8 /* SUMMARY /* Postfix delivery to external command /* SYNOPSIS /* \fBpipe\fR [generic Postfix daemon options] command_attributes... /* DESCRIPTION /* The \fBpipe\fR(8) daemon processes requests from the Postfix queue /* manager to deliver messages to external commands. /* This program expects to be run from the \fBmaster\fR(8) process /* manager. /* /* Message attributes such as sender address, recipient address and /* next-hop host name can be specified as command-line macros that are /* expanded before the external command is executed. /* /* The \fBpipe\fR(8) daemon updates queue files and marks recipients /* as finished, or it informs the queue manager that delivery should /* be tried again at a later time. Delivery status reports are sent /* to the \fBbounce\fR(8), \fBdefer\fR(8) or \fBtrace\fR(8) daemon as /* appropriate. /* SINGLE-RECIPIENT DELIVERY /* .ad /* .fi /* Some destinations cannot handle more than one recipient per /* delivery request. Examples are pagers or fax machines. /* In addition, multi-recipient delivery is undesirable when /* prepending a \fBDelivered-to:\fR or \fBX-Original-To:\fR /* message header. /* /* To prevent Postfix from sending multiple recipients per delivery /* request, specify /* .sp /* .nf /* \fItransport\fB_destination_recipient_limit = 1\fR /* .fi /* /* in the Postfix \fBmain.cf\fR file, where \fItransport\fR /* is the name in the first column of the Postfix \fBmaster.cf\fR /* entry for the pipe-based delivery transport. /* COMMAND ATTRIBUTE SYNTAX /* .ad /* .fi /* The external command attributes are given in the \fBmaster.cf\fR /* file at the end of a service definition. The syntax is as follows: /* .IP "\fBchroot=\fIpathname\fR (optional)" /* Change the process root directory and working directory to /* the named directory. This happens before switching to the /* privileges specified with the \fBuser\fR attribute, and /* before executing the optional \fBdirectory=\fIpathname\fR /* directive. Delivery is deferred in case of failure. /* .sp /* This feature is available as of Postfix 2.3. /* .IP "\fBdirectory=\fIpathname\fR (optional)" /* Change to the named directory before executing the external command. /* The directory must be accessible for the user specified with the /* \fBuser\fR attribute (see below). /* The default working directory is \fB$queue_directory\fR. /* Delivery is deferred in case of failure. /* .sp /* This feature is available as of Postfix 2.2. /* .IP "\fBeol=\fIstring\fR (optional, default: \fB\en\fR)" /* The output record delimiter. Typically one would use either /* \fB\er\en\fR or \fB\en\fR. The usual C-style backslash escape /* sequences are recognized: \fB\ea \eb \ef \en \er \et \ev /* \e\fIddd\fR (up to three octal digits) and \fB\e\e\fR. /* .IP "\fBflags=BDFORXhqu.>\fR (optional)" /* Optional message processing flags. By default, a message is /* copied unchanged. /* .RS /* .IP \fBB\fR /* Append a blank line at the end of each message. This is required /* by some mail user agents that recognize "\fBFrom \fR" lines only /* when preceded by a blank line. /* .IP \fBD\fR /* Prepend a "\fBDelivered-To: \fIrecipient\fR" message header with the /* envelope recipient address. Note: for this to work, the /* \fItransport\fB_destination_recipient_limit\fR must be 1 /* (see SINGLE-RECIPIENT DELIVERY above for details). /* .sp /* The \fBD\fR flag also enforces loop detection (Postfix 2.5 and later): /* if a message already contains a \fBDelivered-To:\fR header /* with the same recipient address, then the message is /* returned as undeliverable. The address comparison is case /* insensitive. /* .sp /* This feature is available as of Postfix 2.0. /* .IP \fBF\fR /* Prepend a "\fBFrom \fIsender time_stamp\fR" envelope header to /* the message content. /* This is expected by, for example, \fBUUCP\fR software. /* .IP \fBO\fR /* Prepend an "\fBX-Original-To: \fIrecipient\fR" message header /* with the recipient address as given to Postfix. Note: for this to /* work, the \fItransport\fB_destination_recipient_limit\fR must be 1 /* (see SINGLE-RECIPIENT DELIVERY above for details). /* .sp /* This feature is available as of Postfix 2.0. /* .IP \fBR\fR /* Prepend a \fBReturn-Path:\fR message header with the envelope sender /* address. /* .IP \fBX\fR /* Indicate that the external command performs final delivery. /* This flag affects the status reported in "success" DSN /* (delivery status notification) messages, and changes it /* from "relayed" into "delivered". /* .sp /* This feature is available as of Postfix 2.5. /* .IP \fBh\fR /* Fold the command-line \fB$original_recipient\fR and /* \fB$recipient\fR address domain part /* (text to the right of the right-most \fB@\fR character) to /* lower case; fold the entire command-line \fB$domain\fR and /* \fB$nexthop\fR host or domain information to lower case. /* This is recommended for delivery via \fBUUCP\fR. /* .IP \fBq\fR /* Quote white space and other special characters in the command-line /* \fB$sender\fR, \fB$original_recipient\fR and \fB$recipient\fR /* address localparts (text to the /* left of the right-most \fB@\fR character), according to an 8-bit /* transparent version of RFC 822. /* This is recommended for delivery via \fBUUCP\fR or \fBBSMTP\fR. /* .sp /* The result is compatible with the address parsing of command-line /* recipients by the Postfix \fBsendmail\fR(1) mail submission command. /* .sp /* The \fBq\fR flag affects only entire addresses, not the partial /* address information from the \fB$user\fR, \fB$extension\fR or /* \fB$mailbox\fR command-line macros. /* .IP \fBu\fR /* Fold the command-line \fB$original_recipient\fR and /* \fB$recipient\fR address localpart (text to /* the left of the right-most \fB@\fR character) to lower case. /* This is recommended for delivery via \fBUUCP\fR. /* .IP \fB.\fR /* Prepend "\fB.\fR" to lines starting with "\fB.\fR". This is needed /* by, for example, \fBBSMTP\fR software. /* .IP \fB>\fR /* Prepend "\fB>\fR" to lines starting with "\fBFrom \fR". This is expected /* by, for example, \fBUUCP\fR software. /* .RE /* .IP "\fBnull_sender\fR=\fIreplacement\fR (default: MAILER-DAEMON)" /* Replace the null sender address (typically used for delivery /* status notifications) with the specified text /* when expanding the \fB$sender\fR command-line macro, and /* when generating a From_ or Return-Path: message header. /* /* If the null sender replacement text is a non-empty string /* then it is affected by the \fBq\fR flag for address quoting /* in command-line arguments. /* /* The null sender replacement text may be empty; this form /* is recommended for content filters that feed mail back into /* Postfix. The empty sender address is not affected by the /* \fBq\fR flag for address quoting in command-line arguments. /* .sp /* Caution: a null sender address is easily mis-parsed by /* naive software. For example, when the \fBpipe\fR(8) daemon /* executes a command such as: /* .sp /* .nf /* \fIWrong\fR: command -f$sender -- $recipient /* .fi /* .IP /* the command will mis-parse the -f option value when the /* sender address is a null string. For correct parsing, /* specify \fB$sender\fR as an argument by itself: /* .sp /* .nf /* \fIRight\fR: command -f $sender -- $recipient /* .fi /* .IP /* This feature is available as of Postfix 2.3. /* .IP "\fBsize\fR=\fIsize_limit\fR (optional)" /* Don't deliver messages that exceed this size limit (in /* bytes); return them to the sender instead. /* .IP "\fBuser\fR=\fIusername\fR (required)" /* .IP "\fBuser\fR=\fIusername\fR:\fIgroupname\fR" /* Execute the external command with the user ID and group ID of the /* specified \fIusername\fR. The software refuses to execute /* commands with root privileges, or with the privileges of the /* mail system owner. If \fIgroupname\fR is specified, the /* corresponding group ID is used instead of the group ID of /* \fIusername\fR. /* .IP "\fBargv\fR=\fIcommand\fR... (required)" /* The command to be executed. This must be specified as the /* last command attribute. /* The command is executed directly, i.e. without interpretation of /* shell meta characters by a shell command interpreter. /* .sp /* Specify "{" and "}" around command arguments that contain /* whitespace (Postfix 3.0 and later). Whitespace /* after the opening "{" and before the closing "}" is ignored. /* .sp /* In the command argument vector, the following macros are recognized /* and replaced with corresponding information from the Postfix queue /* manager delivery request. /* .sp /* In addition to the form ${\fIname\fR}, the forms $\fIname\fR and /* the deprecated form $(\fIname\fR) are also recognized. /* Specify \fB$$\fR where a single \fB$\fR is wanted. /* .RS /* .IP \fB${client_address}\fR /* This macro expands to the remote client network address. /* .sp /* This feature is available as of Postfix 2.2. /* .IP \fB${client_helo}\fR /* This macro expands to the remote client HELO command parameter. /* .sp /* This feature is available as of Postfix 2.2. /* .IP \fB${client_hostname}\fR /* This macro expands to the remote client hostname. /* .sp /* This feature is available as of Postfix 2.2. /* .IP \fB${client_port}\fR /* This macro expands to the remote client TCP port number. /* .sp /* This feature is available as of Postfix 2.5. /* .IP \fB${client_protocol}\fR /* This macro expands to the remote client protocol. /* .sp /* This feature is available as of Postfix 2.2. /* .IP \fB${domain}\fR /* This macro expands to the domain portion of the recipient /* address. For example, with an address \fIuser+foo@domain\fR /* the domain is \fIdomain\fR. /* .sp /* This information is modified by the \fBh\fR flag for case folding. /* .sp /* This feature is available as of Postfix 2.5. /* .IP \fB${extension}\fR /* This macro expands to the extension part of a recipient address. /* For example, with an address \fIuser+foo@domain\fR the extension is /* \fIfoo\fR. /* .sp /* A command-line argument that contains \fB${extension}\fR expands /* into as many command-line arguments as there are recipients. /* .sp /* This information is modified by the \fBu\fR flag for case folding. /* .IP \fB${mailbox}\fR /* This macro expands to the complete local part of a recipient address. /* For example, with an address \fIuser+foo@domain\fR the mailbox is /* \fIuser+foo\fR. /* .sp /* A command-line argument that contains \fB${mailbox}\fR /* expands to as many command-line arguments as there are recipients. /* .sp /* This information is modified by the \fBu\fR flag for case folding. /* .IP \fB${nexthop}\fR /* This macro expands to the next-hop hostname. /* .sp /* This information is modified by the \fBh\fR flag for case folding. /* .IP \fB${original_recipient}\fR /* This macro expands to the complete recipient address before any /* address rewriting or aliasing. /* .sp /* A command-line argument that contains /* \fB${original_recipient}\fR expands to as many /* command-line arguments as there are recipients. /* .sp /* This information is modified by the \fBhqu\fR flags for quoting /* and case folding. /* .sp /* This feature is available as of Postfix 2.5. /* .IP \fB${queue_id}\fR /* This macro expands to the queue id. /* .sp /* This feature is available as of Postfix 2.11. /* .IP \fB${recipient}\fR /* This macro expands to the complete recipient address. /* .sp /* A command-line argument that contains \fB${recipient}\fR /* expands to as many command-line arguments as there are recipients. /* .sp /* This information is modified by the \fBhqu\fR flags for quoting /* and case folding. /* .IP \fB${sasl_method}\fR /* This macro expands to the name of the SASL authentication /* mechanism in the AUTH command when the Postfix SMTP server /* received the message. /* .sp /* This feature is available as of Postfix 2.2. /* .IP \fB${sasl_sender}\fR /* This macro expands to the SASL sender name (i.e. the original /* submitter as per RFC 4954) in the MAIL FROM command when /* the Postfix SMTP server received the message. /* .sp /* This feature is available as of Postfix 2.2. /* .IP \fB${sasl_username}\fR /* This macro expands to the SASL user name in the AUTH command /* when the Postfix SMTP server received the message. /* .sp /* This feature is available as of Postfix 2.2. /* .IP \fB${sender}\fR /* This macro expands to the envelope sender address. By default, /* the null sender address expands to MAILER-DAEMON; this can /* be changed with the \fBnull_sender\fR attribute, as described /* above. /* .sp /* This information is modified by the \fBq\fR flag for quoting. /* .IP \fB${size}\fR /* This macro expands to Postfix's idea of the message size, which /* is an approximation of the size of the message as delivered. /* .IP \fB${user}\fR /* This macro expands to the username part of a recipient address. /* For example, with an address \fIuser+foo@domain\fR the username /* part is \fIuser\fR. /* .sp /* A command-line argument that contains \fB${user}\fR expands /* into as many command-line arguments as there are recipients. /* .sp /* This information is modified by the \fBu\fR flag for case folding. /* .RE /* STANDARDS /* RFC 3463 (Enhanced status codes) /* DIAGNOSTICS /* Command exit status codes are expected to /* follow the conventions defined in <\fBsysexits.h\fR>. /* Exit status 0 means normal successful completion. /* /* In the case of a non-zero exit status, a limited amount of /* command output is logged, and reported in a delivery status /* notification. When the output begins with a 4.X.X or 5.X.X /* enhanced status code, the status code takes precedence over /* the non-zero exit status (Postfix version 2.3 and later). /* /* After successful delivery (zero exit status) a limited /* amount of command output is logged, and reported in "success" /* delivery status notifications (Postfix 3.0 and later). /* This command output is not examined for the presence of an /* enhanced status code. /* /* Problems and transactions are logged to \fBsyslogd\fR(8) /* or \fBpostlogd\fR(8). /* Corrupted message files are marked so that the queue manager /* can move them to the \fBcorrupt\fR queue for further inspection. /* SECURITY /* .fi /* .ad /* This program needs a dual personality 1) to access the private /* Postfix queue and IPC mechanisms, and 2) to execute external /* commands as the specified user. It is therefore security sensitive. /* CONFIGURATION PARAMETERS /* .ad /* .fi /* Changes to \fBmain.cf\fR are picked up automatically as \fBpipe\fR(8) /* processes run for only a limited amount of time. Use the command /* "\fBpostfix reload\fR" to speed up a change. /* /* The text below provides only a parameter summary. See /* \fBpostconf\fR(5) for more details including examples. /* RESOURCE AND RATE CONTROLS /* .ad /* .fi /* In the text below, \fItransport\fR is the first field in a /* \fBmaster.cf\fR entry. /* .IP "\fBtransport_time_limit ($command_time_limit)\fR" /* A transport-specific override for the command_time_limit parameter /* value, where \fItransport\fR is the master.cf name of the message /* delivery transport. /* .PP /* Implemented in the qmgr(8) daemon: /* .IP "\fBtransport_destination_concurrency_limit ($default_destination_concurrency_limit)\fR" /* A transport-specific override for the /* default_destination_concurrency_limit parameter value, where /* \fItransport\fR is the master.cf name of the message delivery /* transport. /* .IP "\fBtransport_destination_recipient_limit ($default_destination_recipient_limit)\fR" /* A transport-specific override for the /* default_destination_recipient_limit parameter value, where /* \fItransport\fR is the master.cf name of the message delivery /* transport. /* MISCELLANEOUS CONTROLS /* .ad /* .fi /* .IP "\fBconfig_directory (see 'postconf -d' output)\fR" /* The default location of the Postfix main.cf and master.cf /* configuration files. /* .IP "\fBdaemon_timeout (18000s)\fR" /* How much time a Postfix daemon process may take to handle a /* request before it is terminated by a built-in watchdog timer. /* .IP "\fBdelay_logging_resolution_limit (2)\fR" /* The maximal number of digits after the decimal point when logging /* sub-second delay values. /* .IP "\fBexport_environment (see 'postconf -d' output)\fR" /* The list of environment variables that a Postfix process will export /* to non-Postfix processes. /* .IP "\fBipc_timeout (3600s)\fR" /* The time limit for sending or receiving information over an internal /* communication channel. /* .IP "\fBmail_owner (postfix)\fR" /* The UNIX system account that owns the Postfix queue and most Postfix /* daemon processes. /* .IP "\fBmax_idle (100s)\fR" /* The maximum amount of time that an idle Postfix daemon process waits /* for an incoming connection before terminating voluntarily. /* .IP "\fBmax_use (100)\fR" /* The maximal number of incoming connections that a Postfix daemon /* process will service before terminating voluntarily. /* .IP "\fBprocess_id (read-only)\fR" /* The process ID of a Postfix command or daemon process. /* .IP "\fBprocess_name (read-only)\fR" /* The process name of a Postfix command or daemon process. /* .IP "\fBqueue_directory (see 'postconf -d' output)\fR" /* The location of the Postfix top-level queue directory. /* .IP "\fBrecipient_delimiter (empty)\fR" /* The set of characters that can separate a user name from its /* extension (example: user+foo), or a .forward file name from its /* extension (example: .forward+foo). /* .IP "\fBsyslog_facility (mail)\fR" /* The syslog facility of Postfix logging. /* .IP "\fBsyslog_name (see 'postconf -d' output)\fR" /* A prefix that is prepended to the process name in syslog /* records, so that, for example, "smtpd" becomes "prefix/smtpd". /* .PP /* Available in Postfix version 3.0 and later: /* .IP "\fBpipe_delivery_status_filter ($default_delivery_status_filter)\fR" /* Optional filter for the \fBpipe\fR(8) delivery agent to change the /* delivery status code or explanatory text of successful or unsuccessful /* deliveries. /* .PP /* Available in Postfix version 3.3 and later: /* .IP "\fBenable_original_recipient (yes)\fR" /* Enable support for the original recipient address after an /* address is rewritten to a different address (for example with /* aliasing or with canonical mapping). /* .IP "\fBservice_name (read-only)\fR" /* The master.cf service name of a Postfix daemon process. /* SEE ALSO /* qmgr(8), queue manager /* bounce(8), delivery status reports /* postconf(5), configuration parameters /* master(5), generic daemon options /* master(8), process manager /* postlogd(8), Postfix logging /* syslogd(8), system logging /* 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 #include #include #include #include #include #include #ifdef STRCASECMP_IN_STRINGS_H #include #endif /* Utility library. */ #include #include #include #include #include #include #include #include #include #include #include #include /* Global library. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* Single server skeleton. */ #include /* Application-specific. */ /* * The mini symbol table name and keys used for expanding macros in * command-line arguments. * * XXX Update the parse_callback() routine when something gets added here, * even when the macro is not recipient dependent. */ #define PIPE_DICT_TABLE "pipe_command" /* table name */ #define PIPE_DICT_NEXTHOP "nexthop" /* key */ #define PIPE_DICT_RCPT "recipient" /* key */ #define PIPE_DICT_ORIG_RCPT "original_recipient" /* key */ #define PIPE_DICT_SENDER "sender"/* key */ #define PIPE_DICT_USER "user" /* key */ #define PIPE_DICT_EXTENSION "extension" /* key */ #define PIPE_DICT_MAILBOX "mailbox" /* key */ #define PIPE_DICT_DOMAIN "domain"/* key */ #define PIPE_DICT_SIZE "size" /* key */ #define PIPE_DICT_CLIENT_ADDR "client_address" /* key */ #define PIPE_DICT_CLIENT_NAME "client_hostname" /* key */ #define PIPE_DICT_CLIENT_PORT "client_port" /* key */ #define PIPE_DICT_CLIENT_PROTO "client_protocol" /* key */ #define PIPE_DICT_CLIENT_HELO "client_helo" /* key */ #define PIPE_DICT_SASL_METHOD "sasl_method" /* key */ #define PIPE_DICT_SASL_USERNAME "sasl_username" /* key */ #define PIPE_DICT_SASL_SENDER "sasl_sender" /* key */ #define PIPE_DICT_QUEUE_ID "queue_id" /* key */ /* * Flags used to pass back the type of special parameter found by * parse_callback. */ #define PIPE_FLAG_RCPT (1<<0) #define PIPE_FLAG_USER (1<<1) #define PIPE_FLAG_EXTENSION (1<<2) #define PIPE_FLAG_MAILBOX (1<<3) #define PIPE_FLAG_DOMAIN (1<<4) #define PIPE_FLAG_ORIG_RCPT (1<<5) /* * Additional flags. These are colocated with mail_copy() flags. Allow some * space for extension of the mail_copy() interface. */ #define PIPE_OPT_FOLD_BASE (16) #define PIPE_OPT_FOLD_USER (FOLD_ADDR_USER << PIPE_OPT_FOLD_BASE) #define PIPE_OPT_FOLD_HOST (FOLD_ADDR_HOST << PIPE_OPT_FOLD_BASE) #define PIPE_OPT_QUOTE_LOCAL (1 << (PIPE_OPT_FOLD_BASE + 2)) #define PIPE_OPT_FINAL_DELIVERY (1 << (PIPE_OPT_FOLD_BASE + 3)) #define PIPE_OPT_FOLD_ALL (FOLD_ADDR_ALL << PIPE_OPT_FOLD_BASE) #define PIPE_OPT_FOLD_FLAGS(f) \ (((f) & PIPE_OPT_FOLD_ALL) >> PIPE_OPT_FOLD_BASE) /* * Tunable parameters. Values are taken from the config file, after * prepending the service name to _name, and so on. */ int var_command_maxtime; /* You can now leave this here. */ /* * Other main.cf parameters. */ char *var_pipe_dsn_filter; /* * For convenience. Instead of passing around lists of parameters, bundle * them up in convenient structures. */ /* * Structure for service-specific configuration parameters. */ typedef struct { int time_limit; /* per-service time limit */ } PIPE_PARAMS; /* * Structure for command-line parameters. */ typedef struct { char **command; /* argument vector */ uid_t uid; /* command privileges */ gid_t gid; /* command privileges */ int flags; /* mail_copy() flags */ char *exec_dir; /* working directory */ char *chroot_dir; /* chroot directory */ VSTRING *eol; /* output record delimiter */ VSTRING *null_sender; /* null sender expansion */ off_t size_limit; /* max size in bytes we will accept */ } PIPE_ATTR; /* * Structure for command-line parameter macro expansion. */ typedef struct { const char *service; /* for warnings */ int expand_flag; /* callback result */ } PIPE_STATE; /* * Silly little macros. */ #define STR vstring_str /* parse_callback - callback for mac_parse() */ static int parse_callback(int type, VSTRING *buf, void *context) { PIPE_STATE *state = (PIPE_STATE *) context; struct cmd_flags { const char *name; int flags; }; static struct cmd_flags cmd_flags[] = { PIPE_DICT_NEXTHOP, 0, PIPE_DICT_RCPT, PIPE_FLAG_RCPT, PIPE_DICT_ORIG_RCPT, PIPE_FLAG_ORIG_RCPT, PIPE_DICT_SENDER, 0, PIPE_DICT_USER, PIPE_FLAG_USER, PIPE_DICT_EXTENSION, PIPE_FLAG_EXTENSION, PIPE_DICT_MAILBOX, PIPE_FLAG_MAILBOX, PIPE_DICT_DOMAIN, PIPE_FLAG_DOMAIN, PIPE_DICT_SIZE, 0, PIPE_DICT_CLIENT_ADDR, 0, PIPE_DICT_CLIENT_NAME, 0, PIPE_DICT_CLIENT_PORT, 0, PIPE_DICT_CLIENT_PROTO, 0, PIPE_DICT_CLIENT_HELO, 0, PIPE_DICT_SASL_METHOD, 0, PIPE_DICT_SASL_USERNAME, 0, PIPE_DICT_SASL_SENDER, 0, PIPE_DICT_QUEUE_ID, 0, 0, 0, }; struct cmd_flags *p; /* * See if this command-line argument references a special macro. */ if (type == MAC_PARSE_VARNAME) { for (p = cmd_flags; /* see below */ ; p++) { if (p->name == 0) { msg_warn("file %s/%s: service %s: unknown macro name: \"%s\"", var_config_dir, MASTER_CONF_FILE, state->service, vstring_str(buf)); return (MAC_PARSE_ERROR); } else if (strcmp(vstring_str(buf), p->name) == 0) { state->expand_flag |= p->flags; return (0); } } } return (0); } /* morph_recipient - morph a recipient address */ static void morph_recipient(VSTRING *buf, const char *address, int flags) { VSTRING *temp = vstring_alloc(100); /* * Quote the recipient address as appropriate. */ if (flags & PIPE_OPT_QUOTE_LOCAL) quote_822_local(temp, address); else vstring_strcpy(temp, address); /* * Fold the recipient address as appropriate. */ fold_addr(buf, STR(temp), PIPE_OPT_FOLD_FLAGS(flags)); vstring_free(temp); } /* expand_argv - expand macros in the argument vector */ static ARGV *expand_argv(const char *service, char **argv, RECIPIENT_LIST *rcpt_list, int flags) { VSTRING *buf = vstring_alloc(100); ARGV *result; char **cpp; PIPE_STATE state; int i; char *ext; char *dom; /* * This appears to be simple operation (replace $name by its expansion). * However, it becomes complex because a command-line argument that * references $recipient must expand to as many command-line arguments as * there are recipients (that's wat programs called by sendmail expect). * So we parse each command-line argument, and depending on what we find, * we either expand the argument just once, or we expand it once for each * recipient. In either case we end up parsing the command-line argument * twice. The amount of CPU time wasted will be negligible. * * Note: we can't use recursive macro expansion here, because recursion * would screw up mail addresses that contain $ characters. */ #define NO 0 #define EARLY_RETURN(x) { argv_free(result); vstring_free(buf); return (x); } result = argv_alloc(1); for (cpp = argv; *cpp; cpp++) { state.service = service; state.expand_flag = 0; if (mac_parse(*cpp, parse_callback, (void *) &state) & MAC_PARSE_ERROR) EARLY_RETURN(0); if (state.expand_flag == 0) { /* no $recipient etc. */ argv_add(result, dict_eval(PIPE_DICT_TABLE, *cpp, NO), ARGV_END); } else { /* contains $recipient etc. */ for (i = 0; i < rcpt_list->len; i++) { /* * This argument contains $recipient. */ if (state.expand_flag & PIPE_FLAG_RCPT) { morph_recipient(buf, rcpt_list->info[i].address, flags); dict_update(PIPE_DICT_TABLE, PIPE_DICT_RCPT, STR(buf)); } /* * This argument contains $original_recipient. */ if (state.expand_flag & PIPE_FLAG_ORIG_RCPT) { morph_recipient(buf, rcpt_list->info[i].orig_addr, flags); dict_update(PIPE_DICT_TABLE, PIPE_DICT_ORIG_RCPT, STR(buf)); } /* * This argument contains $user. Extract the plain user name. * Either anything to the left of the extension delimiter or, * in absence of the latter, anything to the left of the * rightmost @. * * Beware: if the user name is blank (e.g. +user@host), the * argument is suppressed. This is necessary to allow for * cyrus bulletin-board (global mailbox) delivery. XXX But, * skipping empty user parts will also prevent other * expansions of this specific command-line argument. */ if (state.expand_flag & PIPE_FLAG_USER) { morph_recipient(buf, rcpt_list->info[i].address, flags & PIPE_OPT_FOLD_ALL); if (split_at_right(STR(buf), '@') == 0) msg_warn("no @ in recipient address: %s", rcpt_list->info[i].address); if (*var_rcpt_delim) split_addr(STR(buf), var_rcpt_delim); if (*STR(buf) == 0) continue; dict_update(PIPE_DICT_TABLE, PIPE_DICT_USER, STR(buf)); } /* * This argument contains $extension. Extract the recipient * extension: anything between the leftmost extension * delimiter and the rightmost @. The extension may be blank. */ if (state.expand_flag & PIPE_FLAG_EXTENSION) { morph_recipient(buf, rcpt_list->info[i].address, flags & PIPE_OPT_FOLD_ALL); if (split_at_right(STR(buf), '@') == 0) msg_warn("no @ in recipient address: %s", rcpt_list->info[i].address); if (*var_rcpt_delim == 0 || (ext = split_addr(STR(buf), var_rcpt_delim)) == 0) ext = ""; /* insert null arg */ dict_update(PIPE_DICT_TABLE, PIPE_DICT_EXTENSION, ext); } /* * This argument contains $mailbox. Extract the mailbox name: * anything to the left of the rightmost @. */ if (state.expand_flag & PIPE_FLAG_MAILBOX) { morph_recipient(buf, rcpt_list->info[i].address, flags & PIPE_OPT_FOLD_ALL); if (split_at_right(STR(buf), '@') == 0) msg_warn("no @ in recipient address: %s", rcpt_list->info[i].address); dict_update(PIPE_DICT_TABLE, PIPE_DICT_MAILBOX, STR(buf)); } /* * This argument contains $domain. Extract the domain name: * anything to the right of the rightmost @. */ if (state.expand_flag & PIPE_FLAG_DOMAIN) { morph_recipient(buf, rcpt_list->info[i].address, flags & PIPE_OPT_FOLD_ALL); dom = split_at_right(STR(buf), '@'); if (dom == 0) { msg_warn("no @ in recipient address: %s", rcpt_list->info[i].address); dom = ""; /* insert null arg */ } dict_update(PIPE_DICT_TABLE, PIPE_DICT_DOMAIN, dom); } /* * Done. */ argv_add(result, dict_eval(PIPE_DICT_TABLE, *cpp, NO), ARGV_END); } } } argv_terminate(result); vstring_free(buf); return (result); } /* get_service_params - get service-name dependent config information */ static void get_service_params(PIPE_PARAMS *config, char *service) { const char *myname = "get_service_params"; /* * Figure out the command time limit for this transport. */ config->time_limit = get_mail_conf_time2(service, _MAXTIME, var_command_maxtime, 's', 1, 0); /* * Give the poor tester a clue of what is going on. */ if (msg_verbose) msg_info("%s: time_limit %d", myname, config->time_limit); } /* get_service_attr - get command-line attributes */ static void get_service_attr(PIPE_ATTR *attr, char **argv) { const char *myname = "get_service_attr"; struct passwd *pwd; struct group *grp; char *user; /* user name */ char *group; /* group name */ char *size; /* max message size */ char *cp; /* * Initialize. */ user = 0; group = 0; attr->command = 0; attr->flags = 0; attr->exec_dir = 0; attr->chroot_dir = 0; attr->eol = vstring_strcpy(vstring_alloc(1), "\n"); attr->null_sender = vstring_strcpy(vstring_alloc(1), MAIL_ADDR_MAIL_DAEMON); attr->size_limit = 0; /* * Iterate over the command-line attribute list. */ for ( /* void */ ; *argv != 0; argv++) { /* * flags=stuff */ if (strncasecmp("flags=", *argv, sizeof("flags=") - 1) == 0) { for (cp = *argv + sizeof("flags=") - 1; *cp; cp++) { switch (*cp) { case 'B': attr->flags |= MAIL_COPY_BLANK; break; case 'D': attr->flags |= MAIL_COPY_DELIVERED; break; case 'F': attr->flags |= MAIL_COPY_FROM; break; case 'O': attr->flags |= MAIL_COPY_ORIG_RCPT; break; case 'R': attr->flags |= MAIL_COPY_RETURN_PATH; break; case 'X': attr->flags |= PIPE_OPT_FINAL_DELIVERY; break; case '.': attr->flags |= MAIL_COPY_DOT; break; case '>': attr->flags |= MAIL_COPY_QUOTE; break; case 'h': attr->flags |= PIPE_OPT_FOLD_HOST; break; case 'q': attr->flags |= PIPE_OPT_QUOTE_LOCAL; break; case 'u': attr->flags |= PIPE_OPT_FOLD_USER; break; default: msg_fatal("unknown flag: %c (ignored)", *cp); break; } } } /* * user=username[:groupname] */ else if (strncasecmp("user=", *argv, sizeof("user=") - 1) == 0) { user = *argv + sizeof("user=") - 1; if ((group = split_at(user, ':')) != 0) /* XXX clobbers argv */ if (*group == 0) group = 0; if ((pwd = getpwnam(user)) == 0) msg_fatal("%s: unknown username: %s", myname, user); attr->uid = pwd->pw_uid; if (group != 0) { if ((grp = getgrnam(group)) == 0) msg_fatal("%s: unknown group: %s", myname, group); attr->gid = grp->gr_gid; } else { attr->gid = pwd->pw_gid; } } /* * directory=string */ else if (strncasecmp("directory=", *argv, sizeof("directory=") - 1) == 0) { attr->exec_dir = mystrdup(*argv + sizeof("directory=") - 1); } /* * chroot=string */ else if (strncasecmp("chroot=", *argv, sizeof("chroot=") - 1) == 0) { attr->chroot_dir = mystrdup(*argv + sizeof("chroot=") - 1); } /* * eol=string */ else if (strncasecmp("eol=", *argv, sizeof("eol=") - 1) == 0) { unescape(attr->eol, *argv + sizeof("eol=") - 1); } /* * null_sender=string */ else if (strncasecmp("null_sender=", *argv, sizeof("null_sender=") - 1) == 0) { vstring_strcpy(attr->null_sender, *argv + sizeof("null_sender=") - 1); } /* * size=max_message_size (in bytes) */ else if (strncasecmp("size=", *argv, sizeof("size=") - 1) == 0) { size = *argv + sizeof("size=") - 1; if ((attr->size_limit = off_cvt_string(size)) < 0) msg_fatal("%s: bad size= value: %s", myname, size); } /* * argv=command... */ else if (strncasecmp("argv=", *argv, sizeof("argv=") - 1) == 0) { *argv += sizeof("argv=") - 1; /* XXX clobbers argv */ attr->command = argv; break; } /* * Bad. */ else msg_fatal("unknown attribute name: %s", *argv); } /* * Sanity checks. Verify that every member has an acceptable value. */ if (user == 0) msg_fatal("missing user= command-line attribute"); if (attr->command == 0) msg_fatal("missing argv= command-line attribute"); if (attr->uid == 0) msg_fatal("user= command-line attribute specifies root privileges"); if (attr->uid == var_owner_uid) msg_fatal("user= command-line attribute specifies mail system owner %s", var_mail_owner); if (attr->gid == 0) msg_fatal("user= command-line attribute specifies privileged group id 0"); if (attr->gid == var_owner_gid) msg_fatal("user= command-line attribute specifies mail system owner %s group id %ld", var_mail_owner, (long) attr->gid); if (attr->gid == var_sgid_gid) msg_fatal("user= command-line attribute specifies mail system %s group id %ld", var_sgid_group, (long) attr->gid); /* * Give the poor tester a clue of what is going on. */ if (msg_verbose) msg_info("%s: uid %ld, gid %ld, flags %d, size %ld", myname, (long) attr->uid, (long) attr->gid, attr->flags, (long) attr->size_limit); } /* eval_command_status - do something with command completion status */ static int eval_command_status(int command_status, char *service, DELIVER_REQUEST *request, PIPE_ATTR *attr, DSN_BUF *why) { RECIPIENT *rcpt; int status; int result = 0; int n; char *saved_text; /* * Depending on the result, bounce or defer the message, and mark the * recipient as done where appropriate. */ switch (command_status) { case PIPE_STAT_OK: /* Save the command output before dsb_update() clobbers it. */ vstring_truncate(why->reason, trimblanks(STR(why->reason), VSTRING_LEN(why->reason)) - STR(why->reason)); if (VSTRING_LEN(why->reason) > 0) { VSTRING_TERMINATE(why->reason); saved_text = vstring_export(vstring_sprintf( vstring_alloc(VSTRING_LEN(why->reason)), " (%.100s)", STR(why->reason))); } else saved_text = mystrdup(""); /* uses shared R/O storage */ dsb_update(why, "2.0.0", (attr->flags & PIPE_OPT_FINAL_DELIVERY) ? "delivered" : "relayed", DSB_SKIP_RMTA, DSB_SKIP_REPLY, "delivered via %s service%s", service, saved_text); myfree(saved_text); (void) DSN_FROM_DSN_BUF(why); for (n = 0; n < request->rcpt_list.len; n++) { rcpt = request->rcpt_list.info + n; status = sent(DEL_REQ_TRACE_FLAGS(request->flags), request->queue_id, &request->msg_stats, rcpt, service, &why->dsn); if (status == 0 && (request->flags & DEL_REQ_FLAG_SUCCESS)) deliver_completed(request->fp, rcpt->offset); result |= status; } break; case PIPE_STAT_BOUNCE: case PIPE_STAT_DEFER: (void) DSN_FROM_DSN_BUF(why); for (n = 0; n < request->rcpt_list.len; n++) { rcpt = request->rcpt_list.info + n; /* XXX Maybe encapsulate this with ndr_append(). */ status = (STR(why->status)[0] != '4' ? bounce_append : defer_append) (DEL_REQ_TRACE_FLAGS(request->flags), request->queue_id, &request->msg_stats, rcpt, service, &why->dsn); if (status == 0) deliver_completed(request->fp, rcpt->offset); result |= status; } break; case PIPE_STAT_CORRUPT: /* XXX DSN should we send something? */ result |= DEL_STAT_DEFER; break; default: msg_panic("eval_command_status: bad status %d", command_status); /* NOTREACHED */ } return (result); } /* deliver_message - deliver message with extreme prejudice */ static int deliver_message(DELIVER_REQUEST *request, char *service, char **argv) { const char *myname = "deliver_message"; static PIPE_PARAMS conf; static PIPE_ATTR attr; RECIPIENT_LIST *rcpt_list = &request->rcpt_list; DSN_BUF *why = dsb_create(); VSTRING *buf; ARGV *expanded_argv = 0; int deliver_status; int command_status; ARGV *export_env; const char *sender; #define DELIVER_MSG_CLEANUP() { \ dsb_free(why); \ if (expanded_argv) argv_free(expanded_argv); \ } if (msg_verbose) msg_info("%s: from <%s>", myname, request->sender); /* * Sanity checks. The get_service_params() and get_service_attr() * routines also do some sanity checks. Look up service attributes and * config information only once. This is safe since the information comes * from a trusted source, not from the delivery request. */ if (request->nexthop[0] == 0) msg_fatal("empty nexthop hostname"); if (rcpt_list->len <= 0) msg_fatal("recipient count: %d", rcpt_list->len); if (attr.command == 0) { get_service_params(&conf, service); get_service_attr(&attr, argv); } /* * The D flag cannot be specified for multi-recipient deliveries. */ if ((attr.flags & MAIL_COPY_DELIVERED) && (rcpt_list->len > 1)) { dsb_simple(why, "4.3.5", "mail system configuration error"); deliver_status = eval_command_status(PIPE_STAT_DEFER, service, request, &attr, why); msg_warn("pipe flag `D' requires %s_destination_recipient_limit = 1", service); DELIVER_MSG_CLEANUP(); return (deliver_status); } /* * The O flag cannot be specified for multi-recipient deliveries. */ if ((attr.flags & MAIL_COPY_ORIG_RCPT) && (rcpt_list->len > 1)) { dsb_simple(why, "4.3.5", "mail system configuration error"); deliver_status = eval_command_status(PIPE_STAT_DEFER, service, request, &attr, why); msg_warn("pipe flag `O' requires %s_destination_recipient_limit = 1", service); DELIVER_MSG_CLEANUP(); return (deliver_status); } /* * Check that this agent accepts messages this large. */ if (attr.size_limit != 0 && request->data_size > attr.size_limit) { if (msg_verbose) msg_info("%s: too big: size_limit = %ld, request->data_size = %ld", myname, (long) attr.size_limit, request->data_size); dsb_simple(why, "5.2.3", "message too large"); deliver_status = eval_command_status(PIPE_STAT_BOUNCE, service, request, &attr, why); DELIVER_MSG_CLEANUP(); return (deliver_status); } /* * Don't deliver a trace-only request. */ if (DEL_REQ_TRACE_ONLY(request->flags)) { RECIPIENT *rcpt; int status; int n; deliver_status = 0; dsb_simple(why, "2.0.0", "delivers to command: %s", attr.command[0]); (void) DSN_FROM_DSN_BUF(why); for (n = 0; n < request->rcpt_list.len; n++) { rcpt = request->rcpt_list.info + n; status = sent(DEL_REQ_TRACE_FLAGS(request->flags), request->queue_id, &request->msg_stats, rcpt, service, &why->dsn); if (status == 0 && (request->flags & DEL_REQ_FLAG_SUCCESS)) deliver_completed(request->fp, rcpt->offset); deliver_status |= status; } DELIVER_MSG_CLEANUP(); return (deliver_status); } /* * Report mail delivery loops. By definition, this requires * single-recipient delivery. Don't silently lose recipients. */ if (attr.flags & MAIL_COPY_DELIVERED) { DELIVERED_HDR_INFO *info; RECIPIENT *rcpt; int loop_found; if (request->rcpt_list.len > 1) msg_panic("%s: delivered-to enabled with multi-recipient request", myname); info = delivered_hdr_init(request->fp, request->data_offset, FOLD_ADDR_ALL); rcpt = request->rcpt_list.info; loop_found = delivered_hdr_find(info, rcpt->address); delivered_hdr_free(info); if (loop_found) { dsb_simple(why, "5.4.6", "mail forwarding loop for %s", rcpt->address); deliver_status = eval_command_status(PIPE_STAT_BOUNCE, service, request, &attr, why); DELIVER_MSG_CLEANUP(); return (deliver_status); } } /* * Deliver. Set the nexthop and sender variables, and expand the command * argument vector. Recipients will be expanded on the fly. XXX Rewrite * envelope and header addresses according to transport-specific * rewriting rules. */ if (vstream_fseek(request->fp, request->data_offset, SEEK_SET) < 0) msg_fatal("seek queue file %s: %m", VSTREAM_PATH(request->fp)); /* * A non-empty null sender replacement is subject to the 'q' flag. */ buf = vstring_alloc(10); sender = *request->sender ? request->sender : STR(attr.null_sender); if (*sender && (attr.flags & PIPE_OPT_QUOTE_LOCAL)) { quote_822_local(buf, sender); dict_update(PIPE_DICT_TABLE, PIPE_DICT_SENDER, STR(buf)); } else dict_update(PIPE_DICT_TABLE, PIPE_DICT_SENDER, sender); if (attr.flags & PIPE_OPT_FOLD_HOST) { casefold(buf, request->nexthop); dict_update(PIPE_DICT_TABLE, PIPE_DICT_NEXTHOP, STR(buf)); } else dict_update(PIPE_DICT_TABLE, PIPE_DICT_NEXTHOP, request->nexthop); vstring_sprintf(buf, "%ld", (long) request->data_size); dict_update(PIPE_DICT_TABLE, PIPE_DICT_SIZE, STR(buf)); dict_update(PIPE_DICT_TABLE, PIPE_DICT_CLIENT_ADDR, request->client_addr); dict_update(PIPE_DICT_TABLE, PIPE_DICT_CLIENT_HELO, request->client_helo); dict_update(PIPE_DICT_TABLE, PIPE_DICT_CLIENT_NAME, request->client_name); dict_update(PIPE_DICT_TABLE, PIPE_DICT_CLIENT_PORT, request->client_port); dict_update(PIPE_DICT_TABLE, PIPE_DICT_CLIENT_PROTO, request->client_proto); dict_update(PIPE_DICT_TABLE, PIPE_DICT_SASL_METHOD, request->sasl_method); dict_update(PIPE_DICT_TABLE, PIPE_DICT_SASL_USERNAME, request->sasl_username); dict_update(PIPE_DICT_TABLE, PIPE_DICT_SASL_SENDER, request->sasl_sender); dict_update(PIPE_DICT_TABLE, PIPE_DICT_QUEUE_ID, request->queue_id); vstring_free(buf); if ((expanded_argv = expand_argv(service, attr.command, rcpt_list, attr.flags)) == 0) { dsb_simple(why, "4.3.5", "mail system configuration error"); deliver_status = eval_command_status(PIPE_STAT_DEFER, service, request, &attr, why); DELIVER_MSG_CLEANUP(); return (deliver_status); } export_env = mail_parm_split(VAR_EXPORT_ENVIRON, var_export_environ); command_status = pipe_command(request->fp, why, CA_PIPE_CMD_UID(attr.uid), CA_PIPE_CMD_GID(attr.gid), CA_PIPE_CMD_SENDER(sender), CA_PIPE_CMD_COPY_FLAGS(attr.flags), CA_PIPE_CMD_ARGV(expanded_argv->argv), CA_PIPE_CMD_TIME_LIMIT(conf.time_limit), CA_PIPE_CMD_EOL(STR(attr.eol)), CA_PIPE_CMD_EXPORT(export_env->argv), CA_PIPE_CMD_CWD(attr.exec_dir), CA_PIPE_CMD_CHROOT(attr.chroot_dir), CA_PIPE_CMD_ORIG_RCPT(rcpt_list->info[0].orig_addr), CA_PIPE_CMD_DELIVERED(rcpt_list->info[0].address), CA_PIPE_CMD_END); argv_free(export_env); deliver_status = eval_command_status(command_status, service, request, &attr, why); /* * Clean up. */ DELIVER_MSG_CLEANUP(); return (deliver_status); } /* pipe_service - perform service for client */ static void pipe_service(VSTREAM *client_stream, char *service, char **argv) { DELIVER_REQUEST *request; int status; /* * This routine runs whenever a client connects to the UNIX-domain socket * dedicated to delivery via external command. What we see below is a * little protocol to (1) tell the queue manager that we are ready, (2) * read a request from the queue manager, and (3) report the completion * status of that request. All connection-management stuff is handled by * the common code in single_server.c. */ if ((request = deliver_request_read(client_stream)) != 0) { status = deliver_message(request, service, argv); deliver_request_done(client_stream, request, status); } } /* pre_accept - see if tables have changed */ static void pre_accept(char *unused_name, char **unused_argv) { const char *table; if ((table = dict_changed_name()) != 0) { msg_info("table %s has changed -- restarting", table); exit(0); } } /* drop_privileges - drop privileges most of the time */ static void drop_privileges(char *unused_name, char **unused_argv) { set_eugid(var_owner_uid, var_owner_gid); } /* pre_init - initialize */ static void pre_init(char *unused_name, char **unused_argv) { flush_init(); } MAIL_VERSION_STAMP_DECLARE; /* main - pass control to the single-threaded skeleton */ int main(int argc, char **argv) { static const CONFIG_TIME_TABLE time_table[] = { VAR_COMMAND_MAXTIME, DEF_COMMAND_MAXTIME, &var_command_maxtime, 1, 0, 0, }; static const CONFIG_STR_TABLE str_table[] = { VAR_PIPE_DSN_FILTER, DEF_PIPE_DSN_FILTER, &var_pipe_dsn_filter, 0, 0, 0, }; /* * Fingerprint executables and core dumps. */ MAIL_VERSION_STAMP_ALLOCATE; single_server_main(argc, argv, pipe_service, CA_MAIL_SERVER_TIME_TABLE(time_table), CA_MAIL_SERVER_STR_TABLE(str_table), CA_MAIL_SERVER_PRE_INIT(pre_init), CA_MAIL_SERVER_POST_INIT(drop_privileges), CA_MAIL_SERVER_PRE_ACCEPT(pre_accept), CA_MAIL_SERVER_PRIVILEGED, CA_MAIL_SERVER_BOUNCE_INIT(VAR_PIPE_DSN_FILTER, &var_pipe_dsn_filter), 0); }