summaryrefslogtreecommitdiffstats
path: root/src/global/mail_stream.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/global/mail_stream.c')
-rw-r--r--src/global/mail_stream.c627
1 files changed, 627 insertions, 0 deletions
diff --git a/src/global/mail_stream.c b/src/global/mail_stream.c
new file mode 100644
index 0000000..46cc87e
--- /dev/null
+++ b/src/global/mail_stream.c
@@ -0,0 +1,627 @@
+/*++
+/* NAME
+/* mail_stream 3
+/* SUMMARY
+/* mail stream management
+/* SYNOPSIS
+/* #include <mail_stream.h>
+/*
+/* typedef struct {
+/* .in +4
+/* VSTREAM *stream; /* read/write stream */
+/* char *id; /* queue ID */
+/* struct timeval ctime; /* create time */
+/* private members...
+/* .in -4
+/* } MAIL_STREAM;
+/*
+/* MAIL_STREAM *mail_stream_file(queue, class, service, mode)
+/* const char *queue;
+/* const char *class;
+/* const char *service;
+/* int mode;
+/*
+/* MAIL_STREAM *mail_stream_service(class, service)
+/* const char *class;
+/* const char *service;
+/*
+/* MAIL_STREAM *mail_stream_command(command)
+/* const char *command;
+/*
+/* void mail_stream_cleanup(info)
+/* MAIL_STREAM *info;
+/*
+/* int mail_stream_finish(info, why)
+/* MAIL_STREAM *info;
+/* VSTRING *why;
+/*
+/* void mail_stream_ctl(info, op, ...)
+/* MAIL_STREAM *info;
+/* int op;
+/* DESCRIPTION
+/* This module provides a generic interface to Postfix queue file
+/* format messages to file, to Postfix server, or to external command.
+/* The routines that open a stream return a handle with an initialized
+/* stream and queue id member. The handle is either given to a cleanup
+/* routine, to dispose of a failed request, or to a finish routine, to
+/* complete the request.
+/*
+/* mail_stream_file() opens a mail stream to a newly-created file and
+/* arranges for trigger delivery at finish time. This call never fails.
+/* But it may take forever. The mode argument specifies additional
+/* file permissions that will be OR-ed in when the file is finished.
+/* While embryonic files have mode 0600, finished files have mode 0700.
+/*
+/* mail_stream_command() opens a mail stream to external command,
+/* and receives queue ID information from the command. The result
+/* is a null pointer when the initial handshake fails. The command
+/* is given to the shell only when necessary. At finish time, the
+/* command is expected to send a completion status.
+/*
+/* mail_stream_service() opens a mail stream to Postfix service,
+/* and receives queue ID information from the command. The result
+/* is a null pointer when the initial handshake fails. At finish
+/* time, the daemon is expected to send a completion status.
+/*
+/* mail_stream_cleanup() cancels the operation that was started with
+/* any of the mail_stream_xxx() routines, and destroys the argument.
+/* It is up to the caller to remove incomplete file objects.
+/*
+/* mail_stream_finish() completes the operation that was started with
+/* any of the mail_stream_xxx() routines, and destroys the argument.
+/* The result is any of the status codes defined in <cleanup_user.h>.
+/* It is up to the caller to remove incomplete file objects.
+/* The why argument can be a null pointer.
+/*
+/* mail_stream_ctl() selectively overrides information that
+/* was specified with mail_stream_file(); none of the attributes
+/* are applicable for other mail stream types. The arguments
+/* are a list macros with arguments, terminated with
+/* CA_MAIL_STREAM_CTL_END which has none. The following lists
+/* the macros and the types of the corresponding arguments.
+/* .IP "CA_MAIL_STREAM_CTL_QUEUE(const char *)"
+/* The argument specifies an alternate destination queue. The
+/* queue file is moved to the specified queue before the call
+/* returns. Failure to rename the queue file results in a fatal
+/* error.
+/* .IP "CA_MAIL_STREAM_CTL_CLASS(const char *)"
+/* The argument specifies an alternate trigger class.
+/* .IP "CA_MAIL_STREAM_CTL_SERVICE(const char *)"
+/* The argument specifies an alternate trigger service.
+/* .IP "CA_MAIL_STREAM_CTL_MODE(int)"
+/* The argument specifies alternate permissions that override
+/* the permissions specified with mail_stream_file().
+/* .IP "CA_MAIL_STREAM_CTL_DELAY(int)"
+/* Attempt to postpone initial delivery by advancing the queue
+/* file modification time stamp by this amount. This has
+/* effect only within the deferred mail queue.
+/* This feature may have no effect with remote file systems.
+/* 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 <unistd.h>
+#include <errno.h>
+#include <utime.h>
+#include <string.h>
+#include <stdarg.h>
+
+/* Utility library. */
+
+#include <msg.h>
+#include <mymalloc.h>
+#include <vstring.h>
+#include <vstream.h>
+#include <stringops.h>
+#include <argv.h>
+#include <sane_fsops.h>
+#include <warn_stat.h>
+
+/* Global library. */
+
+#include <cleanup_user.h>
+#include <mail_proto.h>
+#include <mail_queue.h>
+#include <opened.h>
+#include <mail_params.h>
+#include <mail_stream.h>
+#include <mail_parm_split.h>
+
+/* Application-specific. */
+
+static VSTRING *id_buf;
+
+#define FREE_AND_WIPE(free, arg) do { if (arg) free(arg); arg = 0; } while (0)
+
+#define STR(x) vstring_str(x)
+
+/* mail_stream_cleanup - clean up after success or failure */
+
+void mail_stream_cleanup(MAIL_STREAM *info)
+{
+ if (info->stream && info->close(info->stream))
+ msg_warn("mail_stream_cleanup: close error");
+ FREE_AND_WIPE(myfree, info->queue);
+ FREE_AND_WIPE(myfree, info->id);
+ FREE_AND_WIPE(myfree, info->class);
+ FREE_AND_WIPE(myfree, info->service);
+ myfree((void *) info);
+}
+
+#if defined(HAS_FUTIMES_AT)
+#define CAN_STAMP_BY_STREAM
+
+/* stamp_stream - update open file [am]time stamp */
+
+static int stamp_stream(VSTREAM *fp, time_t when)
+{
+ struct timeval tv[2];
+
+ if (when != 0) {
+ tv[0].tv_sec = tv[1].tv_sec = when;
+ tv[0].tv_usec = tv[1].tv_usec = 0;
+ return (futimesat(vstream_fileno(fp), (char *) 0, tv));
+ } else {
+ return (futimesat(vstream_fileno(fp), (char *) 0, (struct timeval *) 0));
+ }
+}
+
+#elif defined(HAS_FUTIMES)
+#define CAN_STAMP_BY_STREAM
+
+/* stamp_stream - update open file [am]time stamp */
+
+static int stamp_stream(VSTREAM *fp, time_t when)
+{
+ struct timeval tv[2];
+
+ if (when != 0) {
+ tv[0].tv_sec = tv[1].tv_sec = when;
+ tv[0].tv_usec = tv[1].tv_usec = 0;
+ return (futimes(vstream_fileno(fp), tv));
+ } else {
+ return (futimes(vstream_fileno(fp), (struct timeval *) 0));
+ }
+}
+
+#endif
+
+/* stamp_path - update file [am]time stamp by pathname */
+
+static int stamp_path(const char *path, time_t when)
+{
+ struct utimbuf tbuf;
+
+ if (when != 0) {
+ tbuf.actime = tbuf.modtime = when;
+ return (utime(path, &tbuf));
+ } else {
+ return (utime(path, (struct utimbuf *) 0));
+ }
+}
+
+/* mail_stream_finish_file - finish file mail stream */
+
+static int mail_stream_finish_file(MAIL_STREAM *info, VSTRING *unused_why)
+{
+ int status = CLEANUP_STAT_OK;
+ static char wakeup[] = {TRIGGER_REQ_WAKEUP};
+ struct stat st;
+ char *path_to_reset = 0;
+ static int incoming_fs_clock_ok = 0;
+ static int incoming_clock_warned = 0;
+ int check_incoming_fs_clock;
+ int err;
+ time_t want_stamp;
+ time_t expect_stamp;
+
+ /*
+ * Make sure the message makes it to file. Set the execute bit when no
+ * write error was detected. Some people believe that this code has a
+ * problem if the system crashes before fsync() returns; fchmod() could
+ * take effect before all the data blocks are written. Wietse claims that
+ * this is not a problem. Postfix rejects incomplete queue files, even
+ * when the +x attribute is set. Every Postfix queue file record has a
+ * type code and a length field. Files with missing records are rejected,
+ * as are files with unknown record type codes. Every Postfix queue file
+ * must end with an explicit END record. Postfix queue files without END
+ * record are discarded.
+ *
+ * Attempt to detect file system clocks that are ahead of local time, but
+ * don't check the file system clock all the time. The effect of file
+ * system clock drift can be difficult to understand (Postfix ignores new
+ * mail until the local clock catches up with the file mtime stamp).
+ *
+ * This clock drift detection code may not work with file systems that work
+ * on a local copy of the file and that update the server only after the
+ * file is closed.
+ *
+ * Optionally set a cooldown time.
+ *
+ * XXX: We assume that utime() does control the file modification time even
+ * when followed by an fchmod(), fsync(), close() sequence. This may fail
+ * with remote file systems when fsync() actually updates the file. Even
+ * then, we still delay the average message by 1/2 of the
+ * queue_run_delay.
+ *
+ * XXX: Victor does not like running utime() after the close(), since this
+ * creates a race even with local filesystems. But Wietse is not
+ * confident that utime() before fsync() and close() will work reliably
+ * with remote file systems.
+ *
+ * XXX Don't run the clock skew tests with Postfix sendmail submissions.
+ * Don't whine against unsuspecting users or applications.
+ */
+ check_incoming_fs_clock =
+ (!incoming_fs_clock_ok && !strcmp(info->queue, MAIL_QUEUE_INCOMING));
+
+#ifdef DELAY_ACTION
+ if (strcmp(info->queue, MAIL_QUEUE_DEFERRED) != 0)
+ info->delay = 0;
+ if (info->delay > 0)
+ want_stamp = time((time_t *) 0) + info->delay;
+ else
+#endif
+ want_stamp = 0;
+
+ /*
+ * If we can cheaply set the file time stamp (no pathname lookup) do it
+ * anyway, so that we can avoid whining later about file server/client
+ * clock skew.
+ *
+ * Otherwise, if we must set the file time stamp for delayed delivery, use
+ * whatever means we have to get the job done, no matter if it is
+ * expensive.
+ *
+ * XXX Unfortunately, Linux futimes() is not usable because it uses /proc.
+ * This may not be available because of chroot, or because of access
+ * restrictions after a process changes privileges.
+ */
+ if (vstream_fflush(info->stream)
+#ifdef CAN_STAMP_BY_STREAM
+ || stamp_stream(info->stream, want_stamp)
+#else
+ || (want_stamp && stamp_path(VSTREAM_PATH(info->stream), want_stamp))
+#endif
+ || fchmod(vstream_fileno(info->stream), 0700 | info->mode)
+#ifdef HAS_FSYNC
+ || fsync(vstream_fileno(info->stream))
+#endif
+ || (check_incoming_fs_clock
+ && fstat(vstream_fileno(info->stream), &st) < 0)
+ )
+ status = (errno == EFBIG ? CLEANUP_STAT_SIZE : CLEANUP_STAT_WRITE);
+#ifdef TEST
+ st.st_mtime += 10;
+#endif
+
+ /*
+ * Work around file system clock skew. If the file system clock is ahead
+ * of the local clock, Postfix won't deliver mail immediately, which is
+ * bad for performance. If the file system clock falls behind the local
+ * clock, it just looks silly in mail headers.
+ */
+ if (status == CLEANUP_STAT_OK && check_incoming_fs_clock) {
+ /* Do NOT use time() result from before fsync(). */
+ expect_stamp = want_stamp ? want_stamp : time((time_t *) 0);
+ if (st.st_mtime > expect_stamp) {
+ path_to_reset = mystrdup(VSTREAM_PATH(info->stream));
+ if (incoming_clock_warned == 0) {
+ msg_warn("file system clock is %d seconds ahead of local clock",
+ (int) (st.st_mtime - expect_stamp));
+ msg_warn("resetting file time stamps - this hurts performance");
+ incoming_clock_warned = 1;
+ }
+ } else {
+ if (st.st_mtime < expect_stamp - 100)
+ msg_warn("file system clock is %d seconds behind local clock",
+ (int) (expect_stamp - st.st_mtime));
+ incoming_fs_clock_ok = 1;
+ }
+ }
+
+ /*
+ * Close the queue file and mark it as closed. Be prepared for
+ * vstream_fclose() to fail even after vstream_fflush() and fsync()
+ * reported no error. Reason: after a file is closed, some networked file
+ * systems copy the file out to another machine. Running the queue on a
+ * remote file system is not recommended, if only for performance
+ * reasons.
+ */
+ err = info->close(info->stream);
+ info->stream = 0;
+ if (status == CLEANUP_STAT_OK && err != 0)
+ status = (errno == EFBIG ? CLEANUP_STAT_SIZE : CLEANUP_STAT_WRITE);
+
+ /*
+ * Work around file system clocks that are ahead of local time.
+ */
+ if (path_to_reset != 0) {
+ if (status == CLEANUP_STAT_OK) {
+ if (stamp_path(path_to_reset, expect_stamp) < 0 && errno != ENOENT)
+ msg_fatal("%s: update file time stamps: %m", info->id);
+ }
+ myfree(path_to_reset);
+ }
+
+ /*
+ * When all is well, notify the next service that a new message has been
+ * queued.
+ */
+ if (status == CLEANUP_STAT_OK && info->class && info->service)
+ mail_trigger(info->class, info->service, wakeup, sizeof(wakeup));
+
+ /*
+ * Cleanup.
+ */
+ mail_stream_cleanup(info);
+ return (status);
+}
+
+/* mail_stream_finish_ipc - finish IPC mail stream */
+
+static int mail_stream_finish_ipc(MAIL_STREAM *info, VSTRING *why)
+{
+ int status = CLEANUP_STAT_WRITE;
+
+ /*
+ * Receive the peer's completion status.
+ */
+ if ((why && attr_scan(info->stream, ATTR_FLAG_STRICT,
+ RECV_ATTR_INT(MAIL_ATTR_STATUS, &status),
+ RECV_ATTR_STR(MAIL_ATTR_WHY, why),
+ ATTR_TYPE_END) != 2)
+ || (!why && attr_scan(info->stream, ATTR_FLAG_MISSING,
+ RECV_ATTR_INT(MAIL_ATTR_STATUS, &status),
+ ATTR_TYPE_END) != 1))
+ status = CLEANUP_STAT_WRITE;
+
+ /*
+ * Cleanup.
+ */
+ mail_stream_cleanup(info);
+ return (status);
+}
+
+/* mail_stream_finish - finish action */
+
+int mail_stream_finish(MAIL_STREAM *info, VSTRING *why)
+{
+ return (info->finish(info, why));
+}
+
+/* mail_stream_file - destination is file */
+
+MAIL_STREAM *mail_stream_file(const char *queue, const char *class,
+ const char *service, int mode)
+{
+ struct timeval tv;
+ MAIL_STREAM *info;
+ VSTREAM *stream;
+
+ stream = mail_queue_enter(queue, 0600 | mode, &tv);
+ if (msg_verbose)
+ msg_info("open %s", VSTREAM_PATH(stream));
+
+ info = (MAIL_STREAM *) mymalloc(sizeof(*info));
+ info->stream = stream;
+ info->finish = mail_stream_finish_file;
+ info->close = vstream_fclose;
+ info->queue = mystrdup(queue);
+ info->id = mystrdup(basename(VSTREAM_PATH(stream)));
+ info->class = mystrdup(class);
+ info->service = mystrdup(service);
+ info->mode = mode;
+#ifdef DELAY_ACTION
+ info->delay = 0;
+#endif
+ info->ctime = tv;
+ return (info);
+}
+
+/* mail_stream_service - destination is service */
+
+MAIL_STREAM *mail_stream_service(const char *class, const char *name)
+{
+ VSTREAM *stream;
+ MAIL_STREAM *info;
+
+ if (id_buf == 0)
+ id_buf = vstring_alloc(10);
+
+ stream = mail_connect_wait(class, name);
+ if (attr_scan(stream, ATTR_FLAG_STRICT,
+ RECV_ATTR_STREQ(MAIL_ATTR_PROTO, MAIL_ATTR_PROTO_CLEANUP),
+ RECV_ATTR_STR(MAIL_ATTR_QUEUEID, id_buf), 0) != 1) {
+ vstream_fclose(stream);
+ return (0);
+ } else {
+ info = (MAIL_STREAM *) mymalloc(sizeof(*info));
+ info->stream = stream;
+ info->finish = mail_stream_finish_ipc;
+ info->close = vstream_fclose;
+ info->queue = 0;
+ info->id = mystrdup(vstring_str(id_buf));
+ info->class = 0;
+ info->service = 0;
+ return (info);
+ }
+}
+
+/* mail_stream_command - destination is command */
+
+MAIL_STREAM *mail_stream_command(const char *command)
+{
+ VSTREAM *stream;
+ MAIL_STREAM *info;
+ ARGV *export_env;
+ int status;
+
+ if (id_buf == 0)
+ id_buf = vstring_alloc(10);
+
+ /*
+ * Treat fork() failure as a transient problem. Treat bad handshake as a
+ * permanent error.
+ *
+ * XXX Are we invoking a Postfix process or a non-Postfix process? In the
+ * former case we can share the full environment; in the latter case only
+ * a restricted environment should be propagated. Even though we are
+ * talking a Postfix-internal protocol there is no way we can tell what
+ * is being executed except by duplicating a lot of existing code.
+ */
+ export_env = mail_parm_split(VAR_EXPORT_ENVIRON, var_export_environ);
+ while ((stream = vstream_popen(O_RDWR,
+ CA_VSTREAM_POPEN_COMMAND(command),
+ CA_VSTREAM_POPEN_EXPORT(export_env->argv),
+ CA_VSTREAM_POPEN_END)) == 0) {
+ msg_warn("fork: %m");
+ sleep(10);
+ }
+ argv_free(export_env);
+ vstream_control(stream,
+ CA_VSTREAM_CTL_PATH(command),
+ CA_VSTREAM_CTL_END);
+
+ if (attr_scan(stream, ATTR_FLAG_STRICT,
+ RECV_ATTR_STREQ(MAIL_ATTR_PROTO, MAIL_ATTR_PROTO_POSTDROP),
+ RECV_ATTR_STR(MAIL_ATTR_QUEUEID, id_buf), 0) != 1) {
+ if ((status = vstream_pclose(stream)) != 0)
+ msg_warn("command \"%s\" exited with status %d", command, status);
+ return (0);
+ } else {
+ info = (MAIL_STREAM *) mymalloc(sizeof(*info));
+ info->stream = stream;
+ info->finish = mail_stream_finish_ipc;
+ info->close = vstream_pclose;
+ info->queue = 0;
+ info->id = mystrdup(vstring_str(id_buf));
+ info->class = 0;
+ info->service = 0;
+ return (info);
+ }
+}
+
+/* mail_stream_ctl - update file-based mail stream properties */
+
+void mail_stream_ctl(MAIL_STREAM *info, int op,...)
+{
+ const char *myname = "mail_stream_ctl";
+ va_list ap;
+ char *new_queue = 0;
+ char *string_value;
+
+ /*
+ * Sanity check. None of the attributes below are applicable unless the
+ * target is a file-based stream.
+ */
+ if (info->finish != mail_stream_finish_file)
+ msg_panic("%s: attempt to update non-file stream %s",
+ myname, info->id);
+
+ for (va_start(ap, op); op != MAIL_STREAM_CTL_END; op = va_arg(ap, int)) {
+
+ switch (op) {
+
+ /*
+ * Change the queue directory. We do this at the end of this
+ * call.
+ */
+ case MAIL_STREAM_CTL_QUEUE:
+ if ((new_queue = va_arg(ap, char *)) == 0)
+ msg_panic("%s: NULL queue",
+ myname);
+ break;
+
+ /*
+ * Change the service that needs to be notified.
+ */
+ case MAIL_STREAM_CTL_CLASS:
+ FREE_AND_WIPE(myfree, info->class);
+ if ((string_value = va_arg(ap, char *)) != 0)
+ info->class = mystrdup(string_value);
+ break;
+
+ case MAIL_STREAM_CTL_SERVICE:
+ FREE_AND_WIPE(myfree, info->service);
+ if ((string_value = va_arg(ap, char *)) != 0)
+ info->service = mystrdup(string_value);
+ break;
+
+ /*
+ * Change the (finished) file access mode.
+ */
+ case MAIL_STREAM_CTL_MODE:
+ info->mode = va_arg(ap, int);
+ break;
+
+ /*
+ * Advance the (finished) file modification time.
+ */
+#ifdef DELAY_ACTION
+ case MAIL_STREAM_CTL_DELAY:
+ if ((info->delay = va_arg(ap, int)) < 0)
+ msg_panic("%s: bad delay time %d", myname, info->delay);
+ break;
+#endif
+
+ default:
+ msg_panic("%s: bad op code %d", myname, op);
+ }
+ }
+ va_end(ap);
+
+ /*
+ * Rename the queue file after allocating memory for new information, so
+ * that the caller can still remove an embryonic file when memory
+ * allocation fails (there is no risk of deleting the wrong file).
+ *
+ * Wietse opposed the idea to update run-time error handler information
+ * here, because this module wasn't designed to defend against internal
+ * concurrency issues with error handlers that attempt to follow dangling
+ * pointers.
+ *
+ * This code duplicates mail_queue_rename(), except that we need the new
+ * path to update the stream pathname.
+ */
+ if (new_queue != 0 && strcmp(info->queue, new_queue) != 0) {
+ char *saved_queue = info->queue;
+ char *saved_path = mystrdup(VSTREAM_PATH(info->stream));
+ VSTRING *new_path = vstring_alloc(100);
+
+ (void) mail_queue_path(new_path, new_queue, info->id);
+ info->queue = mystrdup(new_queue);
+ vstream_control(info->stream, CA_VSTREAM_CTL_PATH(STR(new_path)),
+ CA_VSTREAM_CTL_END);
+
+ if (sane_rename(saved_path, STR(new_path)) == 0
+ || (mail_queue_mkdirs(STR(new_path)) == 0
+ && sane_rename(saved_path, STR(new_path)) == 0)) {
+ if (msg_verbose)
+ msg_info("%s: placed in %s queue", info->id, info->queue);
+ } else {
+ msg_fatal("%s: move to %s queue failed: %m", info->id,
+ info->queue);
+ }
+
+ myfree(saved_path);
+ myfree(saved_queue);
+ vstring_free(new_path);
+ }
+}