summaryrefslogtreecommitdiffstats
path: root/src/pickup/pickup.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/pickup/pickup.c')
-rw-r--r--src/pickup/pickup.c630
1 files changed, 630 insertions, 0 deletions
diff --git a/src/pickup/pickup.c b/src/pickup/pickup.c
new file mode 100644
index 0000000..157970d
--- /dev/null
+++ b/src/pickup/pickup.c
@@ -0,0 +1,630 @@
+/*++
+/* NAME
+/* pickup 8
+/* SUMMARY
+/* Postfix local mail pickup
+/* SYNOPSIS
+/* \fBpickup\fR [generic Postfix daemon options]
+/* DESCRIPTION
+/* The \fBpickup\fR(8) daemon waits for hints that new mail has been
+/* dropped into the \fBmaildrop\fR directory, and feeds it into the
+/* \fBcleanup\fR(8) daemon.
+/* Ill-formatted files are deleted without notifying the originator.
+/* This program expects to be run from the \fBmaster\fR(8) process
+/* manager.
+/* STANDARDS
+/* .ad
+/* .fi
+/* None. The \fBpickup\fR(8) daemon does not interact with
+/* the outside world.
+/* SECURITY
+/* .ad
+/* .fi
+/* The \fBpickup\fR(8) daemon is moderately security sensitive. It runs
+/* with fixed low privilege and can run in a chrooted environment.
+/* However, the program reads files from potentially hostile users.
+/* The \fBpickup\fR(8) daemon opens no files for writing, is careful about
+/* what files it opens for reading, and does not actually touch any data
+/* that is sent to its public service endpoint.
+/* DIAGNOSTICS
+/* Problems and transactions are logged to \fBsyslogd\fR(8)
+/* or \fBpostlogd\fR(8).
+/* BUGS
+/* The \fBpickup\fR(8) daemon copies mail from file to the \fBcleanup\fR(8)
+/* daemon. It could avoid message copying overhead by sending a file
+/* descriptor instead of file data, but then the already complex
+/* \fBcleanup\fR(8) daemon would have to deal with unfiltered user data.
+/* CONFIGURATION PARAMETERS
+/* .ad
+/* .fi
+/* As the \fBpickup\fR(8) daemon is a relatively long-running process, up
+/* to an hour may pass before a \fBmain.cf\fR change takes effect.
+/* Use the command "\fBpostfix reload\fR" command to speed up a change.
+/*
+/* The text below provides only a parameter summary. See
+/* \fBpostconf\fR(5) for more details including examples.
+/* CONTENT INSPECTION CONTROLS
+/* .ad
+/* .fi
+/* .IP "\fBcontent_filter (empty)\fR"
+/* After the message is queued, send the entire message to the
+/* specified \fItransport:destination\fR.
+/* .IP "\fBreceive_override_options (empty)\fR"
+/* Enable or disable recipient validation, built-in content
+/* filtering, or address mapping.
+/* 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 "\fBipc_timeout (3600s)\fR"
+/* The time limit for sending or receiving information over an internal
+/* communication channel.
+/* .IP "\fBline_length_limit (2048)\fR"
+/* Upon input, long lines are chopped up into pieces of at most
+/* this length; upon delivery, long lines are reconstructed.
+/* .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 "\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 3.3 and later:
+/* .IP "\fBservice_name (read-only)\fR"
+/* The master.cf service name of a Postfix daemon process.
+/* .PP
+/* Available in Postfix 3.5 and later:
+/* .IP "\fBinfo_log_address_format (external)\fR"
+/* The email address form that will be used in non-debug logging
+/* (info, warning, etc.).
+/* SEE ALSO
+/* cleanup(8), message canonicalization
+/* sendmail(1), Sendmail-compatible interface
+/* postdrop(1), mail posting agent
+/* 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 <sys_defs.h>
+#include <sys/stat.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <time.h>
+#include <string.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <ctype.h>
+
+/* Utility library. */
+
+#include <msg.h>
+#include <scan_dir.h>
+#include <vstring.h>
+#include <vstream.h>
+#include <set_ugid.h>
+#include <safe_open.h>
+#include <watchdog.h>
+#include <stringops.h>
+
+/* Global library. */
+
+#include <mail_queue.h>
+#include <mail_open_ok.h>
+#include <mymalloc.h>
+#include <mail_proto.h>
+#include <cleanup_user.h>
+#include <mail_date.h>
+#include <mail_params.h>
+#include <mail_conf.h>
+#include <record.h>
+#include <rec_type.h>
+#include <lex_822.h>
+#include <input_transp.h>
+#include <rec_attr_map.h>
+#include <mail_version.h>
+#include <smtputf8.h>
+#include <info_log_addr_form.h>
+
+/* Single-threaded server skeleton. */
+
+#include <mail_server.h>
+
+/* Application-specific. */
+
+char *var_filter_xport;
+char *var_input_transp;
+
+ /*
+ * Structure to bundle a bunch of information about a queue file.
+ */
+typedef struct {
+ char *id; /* queue file basename */
+ struct stat st; /* queue file status */
+ char *path; /* name for open/remove */
+ char *sender; /* sender address */
+} PICKUP_INFO;
+
+ /*
+ * What action should be taken after attempting to deliver a message: remove
+ * the file from the maildrop, or leave it alone. The latter is also used
+ * for files that are still being written to.
+ */
+#define REMOVE_MESSAGE_FILE 1
+#define KEEP_MESSAGE_FILE 2
+
+ /*
+ * Transparency: before mail is queued, do we allow address mapping,
+ * automatic bcc, header/body checks?
+ */
+int pickup_input_transp_mask;
+
+/* file_read_error - handle error while reading queue file */
+
+static int file_read_error(PICKUP_INFO *info, int type)
+{
+ msg_warn("uid=%ld: unexpected or malformed record type %d",
+ (long) info->st.st_uid, type);
+ return (REMOVE_MESSAGE_FILE);
+}
+
+/* cleanup_service_error_reason - handle error writing to cleanup service. */
+
+static int cleanup_service_error_reason(PICKUP_INFO *info, int status,
+ const char *reason)
+{
+
+ /*
+ * XXX If the cleanup server gave a reason, then it was already logged.
+ * Don't bother logging it another time.
+ *
+ * XXX Discard a message without recipient. This can happen with "postsuper
+ * -r" when a message is already delivered (or bounced). The Postfix
+ * sendmail command rejects submissions without recipients.
+ */
+ if (reason == 0 || *reason == 0)
+ msg_warn("%s: error writing %s: %s",
+ info->path, info->id, cleanup_strerror(status));
+ return ((status & (CLEANUP_STAT_BAD | CLEANUP_STAT_RCPT)) ?
+ REMOVE_MESSAGE_FILE : KEEP_MESSAGE_FILE);
+}
+
+#define cleanup_service_error(info, status) \
+ cleanup_service_error_reason((info), (status), (char *) 0)
+
+/* copy_segment - copy a record group */
+
+static int copy_segment(VSTREAM *qfile, VSTREAM *cleanup, PICKUP_INFO *info,
+ VSTRING *buf, char *expected)
+{
+ int type;
+ int check_first = (*expected == REC_TYPE_CONTENT[0]);
+ int time_seen = 0;
+ char *attr_name;
+ char *attr_value;
+ char *saved_attr;
+ int skip_attr;
+
+ /*
+ * Limit the input record size. All front-end programs should protect the
+ * mail system against unreasonable inputs. This also requires that we
+ * limit the size of envelope records written by the local posting agent.
+ *
+ * Records with named attributes are filtered by postdrop(1).
+ *
+ * We must allow PTR records here because of "postsuper -r".
+ */
+ for (;;) {
+ if ((type = rec_get(qfile, buf, var_line_limit)) < 0
+ || strchr(expected, type) == 0)
+ return (file_read_error(info, type));
+ if (msg_verbose)
+ msg_info("%s: read %c %s", info->id, type, vstring_str(buf));
+ if (type == *expected)
+ break;
+ if (type == REC_TYPE_FROM) {
+ if (info->sender == 0)
+ info->sender = mystrdup(vstring_str(buf));
+ /* Compatibility with Postfix < 2.3. */
+ if (time_seen == 0)
+ rec_fprintf(cleanup, REC_TYPE_TIME, "%ld",
+ (long) info->st.st_mtime);
+ }
+ if (type == REC_TYPE_TIME)
+ time_seen = 1;
+
+ /*
+ * XXX Workaround: REC_TYPE_FILT (used in envelopes) == REC_TYPE_CONT
+ * (used in message content).
+ *
+ * As documented in postsuper(1), ignore content filter record.
+ */
+ if (*expected != REC_TYPE_CONTENT[0]) {
+ if (type == REC_TYPE_FILT)
+ /* Discard FILTER record after "postsuper -r". */
+ continue;
+ if (type == REC_TYPE_RDR)
+ /* Discard REDIRECT record after "postsuper -r". */
+ continue;
+ }
+ if (*expected == REC_TYPE_EXTRACT[0]) {
+ if (type == REC_TYPE_RRTO)
+ /* Discard return-receipt record after "postsuper -r". */
+ continue;
+ if (type == REC_TYPE_ERTO)
+ /* Discard errors-to record after "postsuper -r". */
+ continue;
+ if (type == REC_TYPE_ATTR) {
+ saved_attr = mystrdup(vstring_str(buf));
+ skip_attr = (split_nameval(saved_attr,
+ &attr_name, &attr_value) == 0
+ && rec_attr_map(attr_name) == 0);
+ myfree(saved_attr);
+ /* Discard other/header/body action after "postsuper -r". */
+ if (skip_attr)
+ continue;
+ }
+ }
+
+ /*
+ * XXX Force an empty record when the queue file content begins with
+ * whitespace, so that it won't be considered as being part of our
+ * own Received: header. What an ugly Kluge.
+ */
+ if (check_first
+ && (type == REC_TYPE_NORM || type == REC_TYPE_CONT)) {
+ check_first = 0;
+ if (VSTRING_LEN(buf) > 0 && IS_SPACE_TAB(vstring_str(buf)[0]))
+ rec_put(cleanup, REC_TYPE_NORM, "", 0);
+ }
+ if ((REC_PUT_BUF(cleanup, type, buf)) < 0)
+ return (cleanup_service_error(info, CLEANUP_STAT_WRITE));
+ }
+ return (0);
+}
+
+/* pickup_copy - copy message to cleanup service */
+
+static int pickup_copy(VSTREAM *qfile, VSTREAM *cleanup,
+ PICKUP_INFO *info, VSTRING *buf)
+{
+ time_t now = time((time_t *) 0);
+ int status;
+ char *name;
+
+ /*
+ * Protect against time-warped time stamps. Warn about mail that has been
+ * queued for an excessive amount of time. Allow for some time drift with
+ * network clients that mount the maildrop remotely - especially clients
+ * that can't get their daylight savings offsets right.
+ */
+#define DAY_SECONDS 86400
+#define HOUR_SECONDS 3600
+
+ if (info->st.st_mtime > now + 2 * HOUR_SECONDS) {
+ msg_warn("%s: message dated %ld seconds into the future",
+ info->id, (long) (info->st.st_mtime - now));
+ info->st.st_mtime = now;
+ } else if (info->st.st_mtime < now - DAY_SECONDS) {
+ msg_warn("%s: message has been queued for %d days",
+ info->id, (int) ((now - info->st.st_mtime) / DAY_SECONDS));
+ }
+
+ /*
+ * Add content inspection transport. See also postsuper(1).
+ */
+ if (*var_filter_xport)
+ rec_fprintf(cleanup, REC_TYPE_FILT, "%s", var_filter_xport);
+
+ /*
+ * Copy the message envelope segment. Allow only those records that we
+ * expect to see in the envelope section. The envelope segment must
+ * contain an envelope sender address.
+ */
+ if ((status = copy_segment(qfile, cleanup, info, buf, REC_TYPE_ENVELOPE)) != 0)
+ return (status);
+ if (info->sender == 0) {
+ msg_warn("%s: uid=%ld: no envelope sender",
+ info->id, (long) info->st.st_uid);
+ return (REMOVE_MESSAGE_FILE);
+ }
+
+ /*
+ * For messages belonging to $mail_owner also log the maildrop queue id.
+ * This supports message tracking for mail requeued via "postsuper -r".
+ */
+#define MAIL_IS_REQUEUED(info) \
+ ((info)->st.st_uid == var_owner_uid && ((info)->st.st_mode & S_IROTH) == 0)
+
+ if (MAIL_IS_REQUEUED(info)) {
+ msg_info("%s: uid=%d from=<%s> orig_id=%s", info->id,
+ (int) info->st.st_uid, info_log_addr_form_sender(info->sender),
+ ((name = strrchr(info->path, '/')) != 0 ?
+ name + 1 : info->path));
+ } else {
+ msg_info("%s: uid=%d from=<%s>", info->id,
+ (int) info->st.st_uid, info_log_addr_form_sender(info->sender));
+ }
+
+ /*
+ * Message content segment. Send a dummy message length. Prepend a
+ * Received: header to the message contents. For tracing purposes,
+ * include the message file ownership, without revealing the login name.
+ */
+ rec_fputs(cleanup, REC_TYPE_MESG, "");
+ rec_fprintf(cleanup, REC_TYPE_NORM, "Received: by %s (%s, from userid %ld)",
+ var_myhostname, var_mail_name, (long) info->st.st_uid);
+ rec_fprintf(cleanup, REC_TYPE_NORM, "\tid %s; %s", info->id,
+ mail_date(info->st.st_mtime));
+
+ /*
+ * Copy the message content segment. Allow only those records that we
+ * expect to see in the message content section.
+ */
+ if ((status = copy_segment(qfile, cleanup, info, buf, REC_TYPE_CONTENT)) != 0)
+ return (status);
+
+ /*
+ * Send the segment with information extracted from message headers.
+ * Permit a non-empty extracted segment, so that list manager software
+ * can to output recipients after the message, and so that sysadmins can
+ * re-inject messages after a change of configuration.
+ */
+ rec_fputs(cleanup, REC_TYPE_XTRA, "");
+ if ((status = copy_segment(qfile, cleanup, info, buf, REC_TYPE_EXTRACT)) != 0)
+ return (status);
+
+ /*
+ * There are no errors. Send the end-of-data marker, and get the cleanup
+ * service completion status. XXX Since the pickup service is unable to
+ * bounce, the cleanup service can report only soft errors here.
+ */
+ rec_fputs(cleanup, REC_TYPE_END, "");
+ if (attr_scan(cleanup, ATTR_FLAG_MISSING,
+ RECV_ATTR_INT(MAIL_ATTR_STATUS, &status),
+ RECV_ATTR_STR(MAIL_ATTR_WHY, buf),
+ ATTR_TYPE_END) != 2)
+ return (cleanup_service_error(info, CLEANUP_STAT_WRITE));
+
+ /*
+ * Depending on the cleanup service completion status, delete the message
+ * file, or try again later. Bounces are dealt with by the cleanup
+ * service itself. The master process wakes up the cleanup service every
+ * now and then.
+ */
+ if (status) {
+ return (cleanup_service_error_reason(info, status, vstring_str(buf)));
+ } else {
+ return (REMOVE_MESSAGE_FILE);
+ }
+}
+
+/* pickup_file - initialize for file copy and cleanup */
+
+static int pickup_file(PICKUP_INFO *info)
+{
+ VSTRING *buf = vstring_alloc(100);
+ int status;
+ VSTREAM *qfile;
+ VSTREAM *cleanup;
+ int cleanup_flags;
+
+ /*
+ * Open the submitted file. If we cannot open it, and we're not having a
+ * file descriptor leak problem, delete the submitted file, so that we
+ * won't keep complaining about the same file again and again. XXX
+ * Perhaps we should save "bad" files elsewhere for further inspection.
+ * XXX How can we delete a file when open() fails with ENOENT?
+ */
+ qfile = safe_open(info->path, O_RDONLY | O_NONBLOCK, 0,
+ (struct stat *) 0, -1, -1, buf);
+ if (qfile == 0) {
+ if (errno != ENOENT)
+ msg_warn("open input file %s: %s", info->path, vstring_str(buf));
+ vstring_free(buf);
+ if (errno == EACCES)
+ msg_warn("if this file was created by Postfix < 1.1, then you may have to chmod a+r %s/%s",
+ var_queue_dir, info->path);
+ return (errno == EACCES ? KEEP_MESSAGE_FILE : REMOVE_MESSAGE_FILE);
+ }
+
+ /*
+ * Contact the cleanup service and read the queue ID that it has
+ * allocated. In case of trouble, request that the cleanup service
+ * bounces its copy of the message. because the original input file is
+ * not readable by the bounce service.
+ *
+ * If mail is re-injected with "postsuper -r", disable Milter applications.
+ * If they were run before the mail was queued then there is no need to
+ * run them again. Moreover, the queue file does not contain enough
+ * information to reproduce the exact same SMTP events and Sendmail
+ * macros that Milters received when the mail originally arrived in
+ * Postfix.
+ *
+ * The actual message copying code is in a separate routine, so that it is
+ * easier to implement the many possible error exits without forgetting
+ * to close files, or to release memory.
+ */
+ cleanup_flags =
+ input_transp_cleanup(CLEANUP_FLAG_BOUNCE | CLEANUP_FLAG_MASK_EXTERNAL,
+ pickup_input_transp_mask);
+ /* As documented in postsuper(1). */
+ if (MAIL_IS_REQUEUED(info))
+ cleanup_flags &= ~CLEANUP_FLAG_MILTER;
+ else
+ cleanup_flags |= smtputf8_autodetect(MAIL_SRC_MASK_SENDMAIL);
+
+ cleanup = mail_connect_wait(MAIL_CLASS_PUBLIC, var_cleanup_service);
+ if (attr_scan(cleanup, ATTR_FLAG_STRICT,
+ RECV_ATTR_STR(MAIL_ATTR_QUEUEID, buf),
+ ATTR_TYPE_END) != 1
+ || attr_print(cleanup, ATTR_FLAG_NONE,
+ SEND_ATTR_INT(MAIL_ATTR_FLAGS, cleanup_flags),
+ ATTR_TYPE_END) != 0) {
+ status = KEEP_MESSAGE_FILE;
+ } else {
+ info->id = mystrdup(vstring_str(buf));
+ status = pickup_copy(qfile, cleanup, info, buf);
+ }
+ vstream_fclose(qfile);
+ vstream_fclose(cleanup);
+ vstring_free(buf);
+ return (status);
+}
+
+/* pickup_init - init info structure */
+
+static void pickup_init(PICKUP_INFO *info)
+{
+ info->id = 0;
+ info->path = 0;
+ info->sender = 0;
+}
+
+/* pickup_free - wipe info structure */
+
+static void pickup_free(PICKUP_INFO *info)
+{
+#define SAFE_FREE(x) { if (x) myfree(x); }
+
+ SAFE_FREE(info->id);
+ SAFE_FREE(info->path);
+ SAFE_FREE(info->sender);
+}
+
+/* pickup_service - service client */
+
+static void pickup_service(char *unused_buf, ssize_t unused_len,
+ char *unused_service, char **argv)
+{
+ SCAN_DIR *scan;
+ char *queue_name;
+ PICKUP_INFO info;
+ const char *path;
+ char *id;
+ int file_count;
+
+ /*
+ * Sanity check. This service takes no command-line arguments.
+ */
+ if (argv[0])
+ msg_fatal("unexpected command-line argument: %s", argv[0]);
+
+ /*
+ * Skip over things that we don't want to open, such as files that are
+ * still being written, or garbage. Leave it up to the sysadmin to remove
+ * garbage. Keep scanning the queue directory until we stop removing
+ * files from it.
+ *
+ * When we find a file, stroke the watchdog so that it will not bark while
+ * some application is keeping us busy by injecting lots of mail into the
+ * maildrop directory.
+ */
+ queue_name = MAIL_QUEUE_MAILDROP; /* XXX should be a list */
+ do {
+ file_count = 0;
+ scan = scan_dir_open(queue_name);
+ while ((id = scan_dir_next(scan)) != 0) {
+ if (mail_open_ok(queue_name, id, &info.st, &path) == MAIL_OPEN_YES) {
+ pickup_init(&info);
+ info.path = mystrdup(path);
+ watchdog_pat();
+ if (pickup_file(&info) == REMOVE_MESSAGE_FILE) {
+ if (REMOVE(info.path))
+ msg_warn("remove %s: %m", info.path);
+ else
+ file_count++;
+ }
+ pickup_free(&info);
+ }
+ }
+ scan_dir_close(scan);
+ } while (file_count);
+}
+
+/* post_jail_init - drop privileges */
+
+static void post_jail_init(char *unused_name, char **unused_argv)
+{
+
+ /*
+ * In case master.cf was not updated for unprivileged service.
+ */
+ if (getuid() != var_owner_uid)
+ set_ugid(var_owner_uid, var_owner_gid);
+
+ /*
+ * Initialize the receive transparency options: do we want unknown
+ * recipient checks, do we want address mapping.
+ */
+ pickup_input_transp_mask =
+ input_transp_mask(VAR_INPUT_TRANSP, var_input_transp);
+}
+
+MAIL_VERSION_STAMP_DECLARE;
+
+/* main - pass control to the multi-threaded server skeleton */
+
+int main(int argc, char **argv)
+{
+ static const CONFIG_STR_TABLE str_table[] = {
+ VAR_FILTER_XPORT, DEF_FILTER_XPORT, &var_filter_xport, 0, 0,
+ VAR_INPUT_TRANSP, DEF_INPUT_TRANSP, &var_input_transp, 0, 0,
+ 0,
+ };
+
+ /*
+ * Fingerprint executables and core dumps.
+ */
+ MAIL_VERSION_STAMP_ALLOCATE;
+
+ /*
+ * Use the multi-threaded skeleton, because no-one else should be
+ * monitoring our service socket while this process runs.
+ *
+ * XXX The default watchdog timeout for trigger servers is 1000s, while the
+ * cleanup server watchdog timeout is $daemon_timeout (i.e. several
+ * hours). We override the default 1000s timeout to avoid problems with
+ * slow mail submission. The real problem is of course that the
+ * single-threaded pickup server is not a good solution for mail
+ * submissions.
+ */
+ trigger_server_main(argc, argv, pickup_service,
+ CA_MAIL_SERVER_STR_TABLE(str_table),
+ CA_MAIL_SERVER_POST_INIT(post_jail_init),
+ CA_MAIL_SERVER_SOLITARY,
+ CA_MAIL_SERVER_WATCHDOG(&var_daemon_timeout),
+ 0);
+}