/*++
/* NAME
/*	mail_stream 3
/* SUMMARY
/*	mail stream management
/* SYNOPSIS
/*	#include <mail_stream.h>
/*
/*	typedef struct {
/* .in +4
/*		VSTREAM	*stream;
/*		char	*id;
/*		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;
/* 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.
/*
/*	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.
/* 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
/*--*/

/* System library. */

#include <sys_defs.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <utime.h>
#include <string.h>

/* Utility library. */

#include <msg.h>
#include <mymalloc.h>
#include <vstring.h>
#include <vstream.h>
#include <stringops.h>
#include <argv.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>

/* Application-specific. */

static VSTRING *id_buf;

#define FREE_AND_WIPE(free, arg) { if (arg) free(arg); arg = 0; }

/* mail_stream_cleanup - clean up after success or failure */

void    mail_stream_cleanup(MAIL_STREAM *info)
{
    FREE_AND_WIPE(info->close, info->stream);
    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((char *) info);
}

/* mail_stream_finish_file - finish file mail stream */

static int mail_stream_finish_file(MAIL_STREAM *info, VSTRING *unused_why)
{
    int     status = 0;
    static char wakeup[] = {TRIGGER_REQ_WAKEUP};
    struct stat st;
    time_t  now;
    struct utimbuf tbuf;
    char   *path_to_reset = 0;
    static int incoming_fs_clock_ok = 0;
    static int incoming_clock_warned = 0;
    int     check_incoming_fs_clock;

    /*
     * 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 next queue run).
     * 
     * 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.
     */
    check_incoming_fs_clock =
	(!incoming_fs_clock_ok && !strcmp(info->queue, MAIL_QUEUE_INCOMING));

    if (vstream_fflush(info->stream)
	|| 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 clocks that are ahead of local time.
     */
    if (status == CLEANUP_STAT_OK && check_incoming_fs_clock) {
	if (st.st_mtime <= time(&now)) {
	    incoming_fs_clock_ok = 1;
	} else {
	    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 - now));
		msg_warn("resetting file time stamps - this hurts performance");
		incoming_clock_warned = 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.
     */
    if (info->close(info->stream))
	status = (errno == EFBIG ? CLEANUP_STAT_SIZE : CLEANUP_STAT_WRITE);
    info->stream = 0;

    /*
     * Work around file system clocks that are ahead of local time.
     */
    if (path_to_reset != 0) {
	tbuf.actime = tbuf.modtime = now;
	if (utime(path_to_reset, &tbuf) < 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)
	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,
			  ATTR_TYPE_NUM, MAIL_ATTR_STATUS, &status,
			  ATTR_TYPE_STR, MAIL_ATTR_WHY, why,
			  ATTR_TYPE_END) != 2)
	|| (!why && attr_scan(info->stream, ATTR_FLAG_MISSING,
			      ATTR_TYPE_NUM, 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)
{
    MAIL_STREAM *info;
    VSTREAM *stream;

    stream = mail_queue_enter(queue, 0600 | mode);
    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;
    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_MISSING,
		  ATTR_TYPE_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 = argv_split(var_export_environ, ", \t\r\n");
    while ((stream = vstream_popen(O_RDWR,
				   VSTREAM_POPEN_COMMAND, command,
				   VSTREAM_POPEN_EXPORT, export_env->argv,
				   VSTREAM_POPEN_END)) == 0) {
	msg_warn("fork: %m");
	sleep(10);
    }
    argv_free(export_env);
    vstream_control(stream,
		    VSTREAM_CTL_PATH, command,
		    VSTREAM_CTL_END);

    if (attr_scan(stream, ATTR_FLAG_MISSING,
		  ATTR_TYPE_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);
    }
}
