changeset 20785:3c8e6a9835d8

lib-program-client: Add program-client from pigeonhole
author Aki Tuomi <aki.tuomi@dovecot.fi>
date Mon, 12 Sep 2016 13:02:23 +0300
parents 16517812f2cb
children 10510f5116ff
files configure.ac dovecot-config.in.in src/Makefile.am src/lib-program-client/Makefile.am src/lib-program-client/program-client-local.c src/lib-program-client/program-client-private.h src/lib-program-client/program-client-remote.c src/lib-program-client/program-client.c src/lib-program-client/program-client.h
diffstat 9 files changed, 1479 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/configure.ac	Thu Sep 29 09:51:40 2016 +0300
+++ b/configure.ac	Mon Sep 12 13:02:23 2016 +0300
@@ -2547,7 +2547,7 @@
 dnl **
 
 
-LIBDOVECOT_LA_LIBS='$(top_builddir)/src/lib-dict-extra/libdict_extra.la $(top_builddir)/src/lib-master/libmaster.la $(top_builddir)/src/lib-settings/libsettings.la $(top_builddir)/src/lib-stats/libstats.la $(top_builddir)/src/lib-http/libhttp.la $(top_builddir)/src/lib-fs/libfs.la $(top_builddir)/src/lib-dict/libdict.la $(top_builddir)/src/lib-dns/libdns.la $(top_builddir)/src/lib-imap/libimap.la $(top_builddir)/src/lib-mail/libmail.la $(top_builddir)/src/lib-sasl/libsasl.la $(top_builddir)/src/lib-auth/libauth.la $(top_builddir)/src/lib-charset/libcharset.la $(top_builddir)/src/lib-ssl-iostream/libssl_iostream.la $(top_builddir)/src/lib-dcrypt/libdcrypt.la $(top_builddir)/src/lib-test/libtest.la $(top_builddir)/src/lib/liblib.la'
+LIBDOVECOT_LA_LIBS='$(top_builddir)/src/lib-dict-extra/libdict_extra.la $(top_builddir)/src/lib-master/libmaster.la $(top_builddir)/src/lib-settings/libsettings.la $(top_builddir)/src/lib-stats/libstats.la $(top_builddir)/src/lib-http/libhttp.la $(top_builddir)/src/lib-fs/libfs.la $(top_builddir)/src/lib-dict/libdict.la $(top_builddir)/src/lib-dns/libdns.la $(top_builddir)/src/lib-imap/libimap.la $(top_builddir)/src/lib-mail/libmail.la $(top_builddir)/src/lib-sasl/libsasl.la $(top_builddir)/src/lib-auth/libauth.la $(top_builddir)/src/lib-charset/libcharset.la $(top_builddir)/src/lib-ssl-iostream/libssl_iostream.la $(top_builddir)/src/lib-dcrypt/libdcrypt.la $(top_builddir)/src/lib-program-client/libprogram_client.la $(top_builddir)/src/lib-test/libtest.la $(top_builddir)/src/lib/liblib.la'
 
 if test "$want_shared_libs" = "yes"; then
   LIBDOVECOT_DEPS='$(top_builddir)/src/lib-dovecot/libdovecot.la'
@@ -2909,6 +2909,7 @@
 src/lib-mail/Makefile
 src/lib-master/Makefile
 src/lib-ntlm/Makefile
+src/lib-program-client/Makefile
 src/lib-otp/Makefile
 src/lib-dovecot/Makefile
 src/lib-sasl/Makefile
--- a/dovecot-config.in.in	Thu Sep 29 09:51:40 2016 +0300
+++ b/dovecot-config.in.in	Mon Sep 12 13:02:23 2016 +0300
@@ -22,7 +22,7 @@
 LIBDOVECOT_DSYNC_DEPS="@LIBDOVECOT_DSYNC@"
 LIBDOVECOT_LIBFTS_DEPS="@LIBDOVECOT_LIBFTS@"
 
-LIBDOVECOT_INCLUDE="-I$(incdir) -I$(incdir)/src/lib -I$(incdir)/src/lib-dict -I$(incdir)/src/lib-dns -I$(incdir)/src/lib-http -I$(incdir)/src/lib-mail -I$(incdir)/src/lib-imap -I$(incdir)/src/lib-fs -I$(incdir)/src/lib-charset -I$(incdir)/src/lib-auth -I$(incdir)/src/lib-master -I$(incdir)/src/lib-ssl-iostream -I$(incdir)/src/lib-compression -I$(incdir)/src/lib-settings -I$(incdir)/src/lib-test -I$(incdir)/src/lib-sasl -I$(incdir)/src/lib-stats -I$(incdir)/src/lib-dcrypt"
+LIBDOVECOT_INCLUDE="-I$(incdir) -I$(incdir)/src/lib -I$(incdir)/src/lib-dict -I$(incdir)/src/lib-dns -I$(incdir)/src/lib-http -I$(incdir)/src/lib-mail -I$(incdir)/src/lib-imap -I$(incdir)/src/lib-fs -I$(incdir)/src/lib-charset -I$(incdir)/src/lib-auth -I$(incdir)/src/lib-master -I$(incdir)/src/lib-ssl-iostream -I$(incdir)/src/lib-compression -I$(incdir)/src/lib-settings -I$(incdir)/src/lib-test -I$(incdir)/src/lib-sasl -I$(incdir)/src/lib-stats -I$(incdir)/src/lib-dcrypt -I$(incdir)/src/lib-program-client"
 LIBDOVECOT_LDA_INCLUDE="-I$(incdir)/src/lib-lda -I$(incdir)/src/lda"
 LIBDOVECOT_AUTH_INCLUDE="-I$(incdir)/src/auth"
 LIBDOVECOT_DOVEADM_INCLUDE="-I$(incdir)/src/doveadm"
--- a/src/Makefile.am	Thu Sep 29 09:51:40 2016 +0300
+++ b/src/Makefile.am	Mon Sep 12 13:02:23 2016 +0300
@@ -19,7 +19,8 @@
 	lib-fs \
 	lib-mail \
 	lib-imap \
-	lib-imap-storage
+	lib-imap-storage \
+	lib-program-client
 
 SUBDIRS = \
 	$(LIBDOVECOT_SUBDIRS) \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-program-client/Makefile.am	Mon Sep 12 13:02:23 2016 +0300
@@ -0,0 +1,18 @@
+noinst_LTLIBRARIES = libprogram_client.la
+
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/src/lib
+
+libprogram_client_la_SOURCES = \
+	program-client.c \
+	program-client-local.c \
+	program-client-remote.c
+
+headers = \
+	program-client.h
+
+noinst_HEADERS = \
+	program-client-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-program-client/program-client-local.c	Mon Sep 12 13:02:23 2016 +0300
@@ -0,0 +1,445 @@
+/* Copyright (c) 2002-2016 Dovecot authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "env-util.h"
+#include "execv-const.h"
+#include "array.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+
+#include "program-client-private.h"
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <grp.h>
+
+struct program_client_local {
+	struct program_client client;
+
+	pid_t pid;
+};
+
+static
+void exec_child(const char *bin_path, const char *const *args, const char *const *envs,
+		int in_fd, int out_fd, int *extra_fds, bool drop_stderr)
+{
+	ARRAY_TYPE(const_string) exec_args;
+
+	/* Setup stdin/stdout */
+
+	if (in_fd < 0) {
+		in_fd = open("/dev/null", O_RDONLY);
+
+		if (in_fd == -1)
+			i_fatal("open(/dev/null) failed: %m");
+	}
+	if (out_fd < 0) {
+		out_fd = open("/dev/null", O_WRONLY);
+
+		if (out_fd == -1)
+			i_fatal("open(/dev/null) failed: %m");
+	}
+
+	if (in_fd != STDIN_FILENO && dup2(in_fd, STDIN_FILENO) < 0)
+		i_fatal("dup2(stdin) failed: %m");
+	if (out_fd != STDOUT_FILENO && dup2(out_fd, STDOUT_FILENO) < 0)
+		i_fatal("dup2(stdout) failed: %m");
+
+	if (in_fd != STDIN_FILENO && close(in_fd) < 0)
+		i_error("close(in_fd) failed: %m");
+	if (out_fd != STDOUT_FILENO && (out_fd != in_fd) && close(out_fd) < 0)
+		i_error("close(out_fd) failed: %m");
+
+	/* Drop stderr if requested */
+	if (drop_stderr) {
+		int err_fd = open("/dev/null", O_WRONLY);
+		if (err_fd == -1)
+			i_fatal("open(/dev/null) failed: %m");
+		if (err_fd != STDERR_FILENO) {
+			if (dup2(err_fd, STDERR_FILENO) < 0)
+				i_fatal("dup2(stderr) failed: %m");
+			if (close(err_fd) < 0)
+				i_error("close(err_fd) failed: %m");
+		}
+	}
+
+	/* Setup extra fds */
+	if (extra_fds != NULL) {
+		int *efd;
+		for(efd = extra_fds; *efd != -1; efd += 2) {
+			i_assert(efd[1] != STDIN_FILENO);
+			i_assert(efd[1] != STDOUT_FILENO);
+			i_assert(efd[1] != STDERR_FILENO);
+			if (efd[0] != efd[1]) {
+				if (dup2(efd[0], efd[1]) < 0)
+					i_fatal("dup2(extra_fd=%d) failed: %m",
+						efd[1]);
+			}
+		}
+		for(efd = extra_fds; *efd != -1; efd += 2) {
+			if (efd[0] != efd[1] && efd[0] != STDIN_FILENO &&
+			    efd[0] != STDOUT_FILENO &&
+			    efd[0] != STDERR_FILENO) {
+				if (close(efd[0]) < 0)
+					i_error("close(extra_fd=%d) failed: %m",
+						efd[1]);
+			}
+		}
+	}
+
+	/* Compose argv */
+
+	t_array_init(&exec_args, 16);
+	array_append(&exec_args, &bin_path, 1);
+	if (args != NULL) {
+		for(; *args != NULL; args++)
+			array_append(&exec_args, args, 1);
+	}
+	(void) array_append_space(&exec_args);
+
+	/* Setup environment */
+
+	env_clean();
+	if (envs != NULL) {
+		for(; *envs != NULL; envs++)
+			env_put(*envs);
+	}
+
+	/* Execute */
+
+	args = array_idx(&exec_args, 0);
+	execvp_const(args[0], args);
+}
+
+static
+int program_client_local_connect(struct program_client *pclient)
+{
+	struct program_client_local *slclient = (struct program_client_local *) pclient;
+	int fd_in[2] = { -1, -1 }, fd_out[2] = {-1, -1};
+	struct program_client_extra_fd *efds = NULL;
+	int *parent_extra_fds = NULL, *child_extra_fds = NULL;
+	unsigned int xfd_count = 0, i;
+
+	/* create normal I/O fds */
+	if (pclient->input != NULL) {
+		if (pipe(fd_in) < 0) {
+			i_error("pipe(in) failed: %m");
+			return -1;
+		}
+	}
+	if (pclient->output != NULL || pclient->output_seekable) {
+		if (pipe(fd_out) < 0) {
+			i_error("pipe(out) failed: %m");
+			return -1;
+		}
+	}
+
+	/* create pipes for additional output through side-channel fds */
+	if (array_is_created(&pclient->extra_fds)) {
+		int extra_fd[2];
+
+		efds = array_get_modifiable(&pclient->extra_fds, &xfd_count);
+		if (xfd_count > 0) {
+			parent_extra_fds = t_malloc0(sizeof(int) * xfd_count);
+			child_extra_fds =
+				t_malloc0(sizeof(int) * xfd_count * 2 + 1);
+			for(i = 0; i < xfd_count; i++) {
+				if (pipe(extra_fd) < 0) {
+					i_error("pipe(extra=%d) failed: %m",
+						extra_fd[1]);
+					return -1;
+				}
+				parent_extra_fds[i] = extra_fd[0];
+				child_extra_fds[i * 2 + 0] = extra_fd[1];
+				child_extra_fds[i * 2 + 1] = efds[i].child_fd;
+			}
+			child_extra_fds[xfd_count * 2] = -1;
+		}
+	}
+
+	/* fork child */
+	if ((slclient->pid = fork()) == (pid_t) - 1) {
+		i_error("fork() failed: %m");
+
+		/* clean up */
+		if (fd_in[0] >= 0 && close(fd_in[0]) < 0) {
+			i_error("close(pipe:in:rd) failed: %m");
+		}
+		if (fd_in[1] >= 0 && close(fd_in[1]) < 0) {
+			i_error("close(pipe:in:wr) failed: %m");
+		}
+		if (fd_out[0] >= 0 && close(fd_out[0]) < 0) {
+			i_error("close(pipe:out:rd) failed: %m");
+		}
+		if (fd_out[1] >= 0 && close(fd_out[1]) < 0) {
+			i_error("close(pipe:out:wr) failed: %m");
+		}
+		for(i = 0; i < xfd_count; i++) {
+			if (close(child_extra_fds[i * 2]) < 0) {
+				i_error("close(pipe:extra=%d:wr) failed: %m",
+					child_extra_fds[i * 2 + 1]);
+			}
+			if (close(parent_extra_fds[i]) < 0) {
+				i_error("close(pipe:extra=%d:rd) failed: %m",
+					child_extra_fds[i * 2 + 1]);
+			}
+		}
+		return -1;
+	}
+
+	if (slclient->pid == 0) {
+		unsigned int count;
+		const char *const *envs = NULL;
+
+		/* child */
+		if (fd_in[1] >= 0 && close(fd_in[1]) < 0)
+			i_error("close(pipe:in:wr) failed: %m");
+		if (fd_out[0] >= 0 && close(fd_out[0]) < 0)
+			i_error("close(pipe:out:rd) failed: %m");
+		for(i = 0; i < xfd_count; i++) {
+			if (close(parent_extra_fds[i]) < 0) {
+				i_error("close(pipe:extra=%d:rd) failed: %m",
+					child_extra_fds[i * 2 + 1]);
+			}
+		}
+
+		/* drop privileges if we have any */
+		if (getuid() == 0) {
+			uid_t uid;
+			gid_t gid;
+
+			/* switch back to root */
+			if (seteuid(0) < 0)
+				i_fatal("seteuid(0) failed: %m");
+
+			/* drop gids first */
+			gid = getgid();
+			if (gid == 0 || gid != pclient->set.gid) {
+				if (pclient->set.gid != 0)
+					gid = pclient->set.gid;
+				else
+					gid = getegid();
+			}
+			if (setgroups(1, &gid) < 0)
+				i_fatal("setgroups(%d) failed: %m", gid);
+			if (gid != 0 && setgid(gid) < 0)
+				i_fatal("setgid(%d) failed: %m", gid);
+
+			/* drop uid */
+			if (pclient->set.uid != 0)
+				uid = pclient->set.uid;
+			else
+				uid = geteuid();
+			if (uid != 0 && setuid(uid) < 0)
+				i_fatal("setuid(%d) failed: %m", uid);
+		}
+
+		i_assert(pclient->set.uid == 0 || getuid() != 0);
+		i_assert(pclient->set.gid == 0 || getgid() != 0);
+
+		if (array_is_created(&pclient->envs))
+			envs = array_get(&pclient->envs, &count);
+
+		exec_child(pclient->path, pclient->args, envs,
+			   fd_in[0], fd_out[1], child_extra_fds,
+			   pclient->set.drop_stderr);
+		i_unreached();
+	}
+
+	/* parent */
+	if (fd_in[0] >= 0 && close(fd_in[0]) < 0)
+		i_error("close(pipe:in:rd) failed: %m");
+	if (fd_out[1] >= 0 && close(fd_out[1]) < 0)
+		i_error("close(pipe:out:wr) failed: %m");
+	if (fd_in[1] >= 0) {
+		net_set_nonblock(fd_in[1], TRUE);
+		pclient->fd_out = fd_in[1];
+	}
+	if (fd_out[0] >= 0) {
+		net_set_nonblock(fd_out[0], TRUE);
+		pclient->fd_in = fd_out[0];
+	}
+	for(i = 0; i < xfd_count; i++) {
+		if (close(child_extra_fds[i * 2]) < 0) {
+			i_error("close(pipe:extra=%d:wr) failed: %m",
+				child_extra_fds[i * 2 + 1]);
+		}
+		net_set_nonblock(parent_extra_fds[i], TRUE);
+		efds[i].parent_fd = parent_extra_fds[i];
+	}
+
+	program_client_init_streams(pclient);
+	return program_client_connected(pclient);
+}
+
+static
+int program_client_local_close_output(struct program_client *pclient)
+{
+	int fd_out = pclient->fd_out;
+
+	pclient->fd_out = -1;
+
+	/* Shutdown output; program stdin will get EOF */
+	if (fd_out >= 0 && close(fd_out) < 0) {
+		i_error("close(%s) failed: %m", pclient->path);
+		return -1;
+	}
+	return 1;
+}
+
+static
+int program_client_local_disconnect(struct program_client *pclient, bool force)
+{
+	struct program_client_local *slclient =	(struct program_client_local *) pclient;
+	pid_t pid = slclient->pid, ret;
+	time_t runtime, timeout = 0;
+	int status;
+
+	if (pid < 0) {
+		/* program never started */
+		pclient->exit_code = 0;
+		return 0;
+	}
+
+	slclient->pid = -1;
+
+	/* Calculate timeout */
+	runtime = ioloop_time - pclient->start_time;
+	if (!force && pclient->set.input_idle_timeout_secs > 0 &&
+	    runtime < (time_t) pclient->set.input_idle_timeout_secs)
+		timeout = pclient->set.input_idle_timeout_secs - runtime;
+
+	if (pclient->debug) {
+		i_debug("waiting for program `%s' to finish after %llu seconds",
+			pclient->path, (unsigned long long int) runtime);
+	}
+
+	/* Wait for child to exit */
+	force = force ||
+		(timeout == 0 && pclient->set.input_idle_timeout_secs > 0);
+	if (!force) {
+		alarm(timeout);
+		ret = waitpid(pid, &status, 0);
+		alarm(0);
+	}
+	if (force || ret < 0) {
+		if (!force && errno != EINTR) {
+			i_error("waitpid(%s) failed: %m", pclient->path);
+			(void) kill(pid, SIGKILL);
+			return -1;
+		}
+
+		/* Timed out */
+		force = TRUE;
+		if (pclient->error == PROGRAM_CLIENT_ERROR_NONE)
+			pclient->error = PROGRAM_CLIENT_ERROR_RUN_TIMEOUT;
+		if (pclient->debug) {
+			i_debug("program `%s' execution timed out after %llu seconds: "
+				"sending TERM signal", pclient->path,
+				(unsigned long long int)pclient->set.input_idle_timeout_secs);
+		}
+
+		/* Kill child gently first */
+		if (kill(pid, SIGTERM) < 0) {
+			i_error("failed to send SIGTERM signal to program `%s'",
+				pclient->path);
+			(void) kill(pid, SIGKILL);
+			return -1;
+		}
+
+		/* Wait for it to die (give it some more time) */
+		alarm(5);
+		ret = waitpid(pid, &status, 0);
+		alarm(0);
+		if (ret < 0) {
+			if (errno != EINTR) {
+				i_error("waitpid(%s) failed: %m",
+					pclient->path);
+				(void) kill(pid, SIGKILL);
+				return -1;
+			}
+
+			/* Timed out again */
+			if (pclient->debug) {
+				i_debug("program `%s' execution timed out: sending KILL signal", pclient->path);
+			}
+
+			/* Kill it brutally now */
+			if (kill(pid, SIGKILL) < 0) {
+				i_error("failed to send SIGKILL signal to program `%s'", pclient->path);
+				return -1;
+			}
+
+			/* Now it will die immediately */
+			if (waitpid(pid, &status, 0) < 0) {
+				i_error("waitpid(%s) failed: %m",
+					pclient->path);
+				return -1;
+			}
+		}
+	}
+
+	/* Evaluate child exit status */
+	pclient->exit_code = -1;
+	if (WIFEXITED(status)) {
+		/* Exited */
+		int exit_code = WEXITSTATUS(status);
+
+		if (exit_code != 0) {
+			i_info("program `%s' terminated with non-zero exit code %d", pclient->path, exit_code);
+			pclient->exit_code = 0;
+			return 0;
+		}
+
+		pclient->exit_code = 1;
+		return 1;
+
+	} else if (WIFSIGNALED(status)) {
+		/* Killed with a signal */
+
+		if (force) {
+			i_error("program `%s' was forcibly terminated with signal %d", pclient->path, WTERMSIG(status));
+		} else {
+			i_error("program `%s' terminated abnormally, signal %d",
+				pclient->path, WTERMSIG(status));
+		}
+		return -1;
+
+	} else if (WIFSTOPPED(status)) {
+		/* Stopped */
+		i_error("program `%s' stopped, signal %d",
+			pclient->path, WSTOPSIG(status));
+		return -1;
+	}
+
+	/* Something else */
+	i_error("program `%s' terminated abnormally, return status %d",
+		pclient->path, status);
+	return -1;
+}
+
+struct program_client *
+program_client_local_create(const char *bin_path,
+			    const char *const *args,
+			    const struct program_client_settings *set)
+{
+	struct program_client_local *pclient;
+	pool_t pool;
+
+	pool = pool_alloconly_create("program client local", 1024);
+	pclient = p_new(pool, struct program_client_local, 1);
+	program_client_init(&pclient->client, pool, bin_path, args, set);
+	pclient->client.connect = program_client_local_connect;
+	pclient->client.close_output = program_client_local_close_output;
+	pclient->client.disconnect = program_client_local_disconnect;
+	pclient->pid = -1;
+
+	return &pclient->client;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-program-client/program-client-private.h	Mon Sep 12 13:02:23 2016 +0300
@@ -0,0 +1,69 @@
+/* Copyright (c) 2002-2016 Dovecot authors, see the included COPYING file
+ */
+
+#ifndef PROGRAM_CLIENT_PRIVATE_H
+#define PROGRAM_CLIENT_PRIVATE_H
+
+#include "program-client.h"
+
+enum program_client_error {
+	PROGRAM_CLIENT_ERROR_NONE,
+	PROGRAM_CLIENT_ERROR_CONNECT_TIMEOUT,
+	PROGRAM_CLIENT_ERROR_RUN_TIMEOUT,
+	PROGRAM_CLIENT_ERROR_IO,
+	PROGRAM_CLIENT_ERROR_OTHER
+};
+
+struct program_client_extra_fd {
+	struct program_client *pclient;
+
+	int child_fd, parent_fd;
+	struct istream *input;
+	struct io *io;
+
+	program_client_fd_callback_t *callback;
+	void *context;
+};
+
+struct program_client {
+	pool_t pool;
+	struct program_client_settings set;
+
+	char *path;
+	const char **args;
+	  ARRAY_TYPE(const_string) envs;
+
+	int fd_in, fd_out;
+	struct io *io;
+	struct ioloop *ioloop;
+	struct timeout *to;
+	time_t start_time;
+
+	struct istream *input, *program_input, *seekable_output;
+	struct ostream *output, *program_output;
+	char *temp_prefix;
+
+	  ARRAY(struct program_client_extra_fd) extra_fds;
+
+	enum program_client_error error;
+	int exit_code;
+
+	int (*connect) (struct program_client * pclient);
+	int (*close_output) (struct program_client * pclient);
+	int (*disconnect) (struct program_client * pclient, bool force);
+
+	bool debug:1;
+	bool disconnected:1;
+	bool output_seekable:1;
+};
+
+void program_client_init(struct program_client *pclient, pool_t pool, const char *path,
+			 const char *const *args, const struct program_client_settings *set);
+
+void program_client_init_streams(struct program_client *pclient);
+
+int program_client_connected(struct program_client *pclient);
+
+void program_client_fail(struct program_client *pclient, enum program_client_error error);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-program-client/program-client-remote.c	Mon Sep 12 13:02:23 2016 +0300
@@ -0,0 +1,334 @@
+/* Copyright (c) 2002-2016 Dovecot authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "net.h"
+#include "write-full.h"
+#include "eacces-error.h"
+#include "istream-private.h"
+#include "ostream.h"
+
+#include "program-client-private.h"
+
+#include <unistd.h>
+#include <sys/wait.h>
+#include <sysexits.h>
+
+/*
+ * Script client input stream
+ */
+
+struct program_client_istream {
+	struct istream_private istream;
+
+	struct stat statbuf;
+
+	struct program_client *client;
+};
+
+static
+void program_client_istream_destroy(struct iostream_private *stream)
+{
+	struct program_client_istream *scstream =
+		(struct program_client_istream *) stream;
+
+	i_stream_unref(&scstream->istream.parent);
+}
+
+static ssize_t
+program_client_istream_read(struct istream_private *stream)
+{
+	struct program_client_istream *scstream =
+		(struct program_client_istream *) stream;
+	size_t pos, reserved;
+	ssize_t ret = 0;
+
+	i_stream_skip(stream->parent, stream->skip);
+	stream->skip = 0;
+
+	stream->buffer = i_stream_get_data(stream->parent, &pos);
+
+	reserved = 0;
+	if (stream->buffer != NULL && pos >= 1) {
+		/* retain/hide potential return code at end of buffer */
+		reserved = (stream->buffer[pos - 1] == '\n' && pos > 1 ? 2 : 1);
+		pos -= reserved;
+	}
+
+	if (stream->parent->eof) {
+		if (pos == 0)
+			i_stream_skip(stream->parent, reserved);
+		stream->istream.eof = TRUE;
+		ret = -1;
+	} else
+		do {
+			if ((ret = i_stream_read(stream->parent)) == -2) {
+				return -2;	/* input buffer full */
+			}
+
+			if (ret == 0 || (ret < 0 && !stream->parent->eof))
+				break;
+
+			stream->istream.stream_errno =
+				stream->parent->stream_errno;
+			stream->buffer =
+				i_stream_get_data(stream->parent, &pos);
+
+			if (stream->parent->eof) {
+				/* Check return code at EOF */
+				if (stream->buffer != NULL && pos >= 2 &&
+				    stream->buffer[pos - 1] == '\n') {
+					switch (stream->buffer[pos - 2]) {
+					case '+':
+						scstream->client->exit_code = 1;
+						break;
+					case '-':
+						scstream->client->exit_code = 0;
+						break;
+					default:
+						scstream->client->exit_code =
+							-1;
+					}
+				} else {
+					scstream->client->exit_code = -1;
+				}
+			}
+
+			if (stream->buffer != NULL && pos >= 1) {
+				/* retain/hide potential return code at end of buffer */
+				size_t old_reserved = reserved;
+				ssize_t reserve_mod;
+
+				reserved = (stream->buffer[pos - 1] == '\n' &&
+					    pos > 1 ? 2 : 1);
+				reserve_mod = reserved - old_reserved;
+				pos -= reserved;
+
+				if (ret >= reserve_mod) {
+					ret -= reserve_mod;
+				}
+			}
+
+			if (ret <= 0 && stream->parent->eof) {
+				/* Parent EOF and not more data to return; EOF here as well */
+				if (pos == 0)
+					i_stream_skip(stream->parent, reserved);
+				stream->istream.eof = TRUE;
+				ret = -1;
+			}
+		} while (ret == 0);
+
+	stream->pos = pos;
+
+	i_assert(ret != -1 || stream->istream.eof ||
+		 stream->istream.stream_errno != 0);
+	return ret;
+}
+
+static
+void ATTR_NORETURN program_client_istream_sync(struct istream_private *stream ATTR_UNUSED)
+{
+	i_panic("program_client_istream sync() not implemented");
+}
+
+static
+int program_client_istream_stat(struct istream_private *stream, bool exact)
+{
+	struct program_client_istream *scstream =
+		(struct program_client_istream *) stream;
+	const struct stat *st;
+	int ret;
+
+	/* Stat the original stream */
+	ret = i_stream_stat(stream->parent, exact, &st);
+	if (ret < 0 || st->st_size == -1 || !exact)
+		return ret;
+
+	scstream->statbuf = *st;
+	scstream->statbuf.st_size = -1;
+
+	return ret;
+}
+
+static
+struct istream *program_client_istream_create(struct program_client *program_client,
+					      struct istream *input)
+{
+	struct program_client_istream *scstream;
+
+	scstream = i_new(struct program_client_istream, 1);
+	scstream->client = program_client;
+
+	scstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+	scstream->istream.iostream.destroy = program_client_istream_destroy;
+	scstream->istream.read = program_client_istream_read;
+	scstream->istream.sync = program_client_istream_sync;
+	scstream->istream.stat = program_client_istream_stat;
+
+	scstream->istream.istream.readable_fd = FALSE;
+	scstream->istream.istream.blocking = input->blocking;
+	scstream->istream.istream.seekable = FALSE;
+
+	i_stream_seek(input, 0);
+
+	return i_stream_create(&scstream->istream, input, -1);
+}
+
+/*
+ * Program client
+ */
+
+struct program_client_remote {
+	struct program_client client;
+
+	bool noreply:1;
+};
+
+static
+void program_client_remote_connected(struct program_client *pclient)
+{
+	struct program_client_remote *slclient =
+		(struct program_client_remote *) pclient;
+	const char **args = pclient->args;
+	string_t *str;
+
+	io_remove(&pclient->io);
+	program_client_init_streams(pclient);
+
+	if (!slclient->noreply) {
+		pclient->program_input =
+			program_client_istream_create(pclient, pclient->program_input);
+	}
+
+	str = t_str_new(1024);
+	str_append(str, "VERSION\tscript\t3\t0\n");
+	if (slclient->noreply)
+		str_append(str, "noreply\n");
+	else
+		str_append(str, "-\n");
+	if (args != NULL) {
+		for(; *args != NULL; args++) {
+			str_append(str, *args);
+			str_append_c(str, '\n');
+		}
+	}
+	str_append_c(str, '\n');
+
+	if (o_stream_send(pclient->program_output,
+			  str_data(str), str_len(str)) < 0) {
+		i_error("write(%s) failed: %s",
+			o_stream_get_name(pclient->program_output),
+			o_stream_get_error(pclient->program_output));
+		program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+		return;
+	}
+
+	(void)program_client_connected(pclient);
+}
+
+static
+int program_client_remote_connect(struct program_client *pclient)
+{
+	struct program_client_remote *slclient =
+		(struct program_client_remote *) pclient;
+	int fd;
+
+	if ((fd = net_connect_unix_with_retries(pclient->path, 1000)) < 0) {
+		switch (errno) {
+		case EACCES:
+			i_error("%s",
+				eacces_error_get("net_connect_unix",
+						 pclient->path));
+			return -1;
+		default:
+			i_error("net_connect_unix(%s) failed: %m",
+				pclient->path);
+			return -1;
+		}
+	}
+
+	net_set_nonblock(fd, TRUE);
+
+	pclient->fd_in = (slclient->noreply && pclient->output == NULL &&
+			  !pclient->output_seekable ? -1 : fd);
+	pclient->fd_out = fd;
+	pclient->io =
+		io_add(fd, IO_WRITE, program_client_remote_connected, pclient);
+	return 0;
+}
+
+static
+int program_client_remote_close_output(struct program_client *pclient)
+{
+	int fd_out = pclient->fd_out, fd_in = pclient->fd_in;
+
+	pclient->fd_out = -1;
+
+	/* Shutdown output; program stdin will get EOF */
+	if (fd_out >= 0) {
+		if (fd_in >= 0) {
+			if (shutdown(fd_out, SHUT_WR) < 0 && errno != ENOTCONN) {
+				i_error("shutdown(%s, SHUT_WR) failed: %m",
+					pclient->path);
+				return -1;
+			}
+		} else if (close(fd_out) < 0) {
+			i_error("close(%s) failed: %m", pclient->path);
+			return -1;
+		}
+	}
+
+	return 1;
+}
+
+static
+int program_client_remote_disconnect(struct program_client *pclient, bool force)
+{
+	struct program_client_remote *slclient =
+		(struct program_client_remote *)pclient;
+	int ret = 0;
+
+	if (pclient->error == PROGRAM_CLIENT_ERROR_NONE && !slclient->noreply &&
+	    pclient->program_input != NULL && !force) {
+		const unsigned char *data;
+		size_t size;
+
+		/* Skip any remaining program output and parse the exit code */
+		while ((ret = i_stream_read_more
+			(pclient->program_input, &data, &size)) > 0) {
+			i_stream_skip(pclient->program_input, size);
+		}
+
+		/* Get exit code */
+		if (!pclient->program_input->eof)
+			ret = -1;
+		else
+			ret = pclient->exit_code;
+	} else {
+		ret = 1;
+	}
+
+	return ret;
+}
+
+struct program_client *
+program_client_remote_create(const char *socket_path, const char *const *args,
+			     const struct program_client_settings *set,
+			     bool noreply)
+{
+	struct program_client_remote *pclient;
+	pool_t pool;
+
+	pool = pool_alloconly_create("program client remote", 1024);
+	pclient = p_new(pool, struct program_client_remote, 1);
+	program_client_init(&pclient->client, pool, socket_path, args, set);
+	pclient->client.connect = program_client_remote_connect;
+	pclient->client.close_output = program_client_remote_close_output;
+	pclient->client.disconnect = program_client_remote_disconnect;
+	pclient->noreply = noreply;
+
+	return &pclient->client;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-program-client/program-client.c	Mon Sep 12 13:02:23 2016 +0300
@@ -0,0 +1,554 @@
+/* Copyright (c) 2002-2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "safe-mkstemp.h"
+#include "istream-private.h"
+#include "istream-seekable.h"
+#include "ostream.h"
+
+#include "program-client-private.h"
+
+#include <unistd.h>
+
+#define MAX_OUTPUT_BUFFER_SIZE 16384
+#define MAX_OUTPUT_MEMORY_BUFFER (1024*128)
+
+static int program_client_seekable_fd_callback
+(const char **path_r, void *context)
+{
+	struct program_client *pclient = (struct program_client *)context;
+	string_t *path;
+	int fd;
+
+	path = t_str_new(128);
+	str_append(path, pclient->temp_prefix);
+	fd = safe_mkstemp(path, 0600, (uid_t)-1, (gid_t)-1);
+	if (fd == -1) {
+		i_error("safe_mkstemp(%s) failed: %m", str_c(path));
+		return -1;
+	}
+
+	/* we just want the fd, unlink it */
+	if (i_unlink(str_c(path)) < 0) {
+		/* shouldn't happen.. */
+		i_close_fd(&fd);
+		return -1;
+	}
+
+	*path_r = str_c(path);
+	return fd;
+}
+
+static void program_client_timeout(struct program_client *pclient)
+{
+	i_error("program `%s' execution timed out (> %d secs)",
+		pclient->path, pclient->set.input_idle_timeout_secs);
+	program_client_fail(pclient, PROGRAM_CLIENT_ERROR_RUN_TIMEOUT);
+}
+
+static void program_client_connect_timeout(struct program_client *pclient)
+{
+	i_error("program `%s' socket connection timed out (> %d msecs)",
+		pclient->path, pclient->set.client_connect_timeout_msecs);
+	program_client_fail(pclient, PROGRAM_CLIENT_ERROR_CONNECT_TIMEOUT);
+}
+
+static int program_client_connect(struct program_client *pclient)
+{
+	int ret;
+
+	if (pclient->set.client_connect_timeout_msecs != 0) {
+		pclient->to = timeout_add
+			(pclient->set.client_connect_timeout_msecs,
+				program_client_connect_timeout, pclient);
+	}
+
+	if ((ret=pclient->connect(pclient)) < 0) {
+		program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+		return -1;
+	}
+	return ret;
+}
+
+static int program_client_close_output(struct program_client *pclient)
+{
+	int ret;
+
+	if (pclient->program_output != NULL)
+		o_stream_destroy(&pclient->program_output);
+	if ((ret=pclient->close_output(pclient)) < 0)
+		return -1;
+	pclient->program_output = NULL;
+
+	return ret;
+}
+
+static void program_client_disconnect_extra_fds
+(struct program_client *pclient)
+{
+	struct program_client_extra_fd *efds;
+	unsigned int i, count;
+	
+	if (!array_is_created(&pclient->extra_fds))
+		return;
+
+	efds = array_get_modifiable(&pclient->extra_fds, &count);
+	for (i = 0; i < count; i++) {
+		if (efds[i].input != NULL)
+			i_stream_unref(&efds[i].input);
+		if (efds[i].io != NULL)
+			io_remove(&efds[i].io);
+		if (efds[i].parent_fd != -1 && close(efds[i].parent_fd) < 0)
+			i_error("close(fd=%d) failed: %m", efds[i].parent_fd);
+	}
+}
+
+static void program_client_disconnect
+(struct program_client *pclient, bool force)
+{
+	int ret, error = FALSE;
+
+	if (pclient->ioloop != NULL)
+		io_loop_stop(pclient->ioloop);
+
+	if (pclient->disconnected)
+		return;
+
+	if ((ret=program_client_close_output(pclient)) < 0)
+		error = TRUE;
+
+	program_client_disconnect_extra_fds(pclient);
+	if ((ret=pclient->disconnect(pclient, force)) < 0)
+		error = TRUE;
+
+	if (pclient->program_input != NULL) {
+		if (pclient->output_seekable)
+			i_stream_unref(&pclient->program_input);
+		else
+			i_stream_destroy(&pclient->program_input);
+	} 
+	if (pclient->program_output != NULL)
+		o_stream_destroy(&pclient->program_output);
+
+	if (pclient->to != NULL)
+		timeout_remove(&pclient->to);
+	if (pclient->io != NULL)
+		io_remove(&pclient->io);
+
+	if (pclient->fd_in != -1 && close(pclient->fd_in) < 0)
+		i_error("close(%s) failed: %m", pclient->path);
+	if (pclient->fd_out != -1 && pclient->fd_out != pclient->fd_in
+		&& close(pclient->fd_out) < 0)
+		i_error("close(%s/out) failed: %m", pclient->path);
+	pclient->fd_in = pclient->fd_out = -1;
+	
+	pclient->disconnected = TRUE;
+	if (error && pclient->error == PROGRAM_CLIENT_ERROR_NONE) {
+		pclient->error = PROGRAM_CLIENT_ERROR_OTHER;
+	}
+}
+
+void program_client_fail
+(struct program_client *pclient, enum program_client_error error)
+{
+	if (pclient->error != PROGRAM_CLIENT_ERROR_NONE)
+		return;
+
+	pclient->error = error;
+	program_client_disconnect(pclient, TRUE);
+}
+
+static bool program_client_input_pending(struct program_client *pclient)
+{
+	struct program_client_extra_fd *efds = NULL;
+	unsigned int count, i;
+
+	if (pclient->program_input != NULL &&
+		!pclient->program_input->closed &&
+		!i_stream_is_eof(pclient->program_input)) {
+		return TRUE;
+	}
+
+	if (array_is_created(&pclient->extra_fds)) {
+		efds = array_get_modifiable(&pclient->extra_fds, &count);
+		for (i = 0; i < count; i++) {
+			if (efds[i].input != NULL &&
+				!efds[i].input->closed &&
+				!i_stream_is_eof(efds[i].input)) {
+				return TRUE;
+			}
+		}
+	}
+
+	return FALSE;
+}
+
+static int program_client_program_output(struct program_client *pclient)
+{
+	struct istream *input = pclient->input;
+	struct ostream *output = pclient->program_output;
+	const unsigned char *data;
+	size_t size;
+	int ret = 0;
+
+	if ((ret = o_stream_flush(output)) <= 0) {
+		if (ret < 0) {
+			i_error("write(%s) failed: %s",
+				o_stream_get_name(output),
+				o_stream_get_error(output));
+			program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+		}
+		return ret;
+	}
+
+	if (input != NULL && output != NULL) {
+		do {
+			while ((data=i_stream_get_data(input, &size)) != NULL) {
+				ssize_t sent;
+	
+				if ((sent=o_stream_send(output, data, size)) < 0) {
+					i_error("write(%s) failed: %s",
+						o_stream_get_name(output),
+						o_stream_get_error(output));
+					program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+					return -1;
+				}
+	
+				if (sent == 0)
+					return 0;
+				i_stream_skip(input, sent);
+			}
+		} while ((ret=i_stream_read(input)) > 0);
+
+		if (ret == 0)
+			return 1;
+
+		if (ret < 0) {
+			if (input->stream_errno != 0) {
+				i_error("read(%s) failed: %s",
+					i_stream_get_name(input),
+					i_stream_get_error(input));
+				program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+				return -1;
+			} else if (!i_stream_have_bytes_left(input)) {
+				i_stream_unref(&pclient->input);
+				input = NULL;
+
+				if ((ret = o_stream_flush(output)) <= 0) {
+					if (ret < 0) {
+						i_error("write(%s) failed: %s",
+							o_stream_get_name(output),
+							o_stream_get_error(output));
+						program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+					}
+					return ret;
+				}
+			} 
+		}
+	}
+
+	if (input == NULL) {
+		if (!program_client_input_pending(pclient)) {
+			program_client_disconnect(pclient, FALSE);
+		} else if (program_client_close_output(pclient) < 0) {
+			program_client_fail(pclient, PROGRAM_CLIENT_ERROR_OTHER);
+		}
+	}
+	return 1;
+}
+
+static void program_client_program_input(struct program_client *pclient)
+{
+	struct istream *input = pclient->program_input;
+	struct ostream *output = pclient->output;
+	const unsigned char *data;
+	size_t size;
+	int ret = 0;
+
+	if (pclient->output_seekable && pclient->seekable_output == NULL) {
+		struct istream *input_list[2] = { input, NULL };
+
+		input = i_stream_create_seekable(input_list, MAX_OUTPUT_MEMORY_BUFFER,
+					 program_client_seekable_fd_callback, pclient);
+		i_stream_unref(&pclient->program_input);
+		pclient->program_input = input;
+
+		pclient->seekable_output = input;
+		i_stream_ref(pclient->seekable_output);
+	}
+
+	if (input != NULL) {
+		while ((ret=i_stream_read_data(input, &data, &size, 0)) > 0) {
+			if (output != NULL) {
+				ssize_t sent;
+
+				if ((sent=o_stream_send(output, data, size)) < 0) {
+					i_error("write(%s) failed: %s",
+						o_stream_get_name(output),
+						o_stream_get_error(output));
+					program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+					return;
+				}
+				size = (size_t)sent;
+			}
+
+			i_stream_skip(input, size);
+		}
+
+		if (ret < 0) {
+			if (input->stream_errno != 0) {
+				i_error("read(%s) failed: %s",
+					i_stream_get_name(input),
+					i_stream_get_error(input));
+				program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+			} else {
+				if (!program_client_input_pending(pclient))
+					program_client_disconnect(pclient, FALSE);
+			}
+		}
+	}
+}
+
+static void program_client_extra_fd_input
+(struct program_client_extra_fd *efd)
+{
+	struct program_client *pclient = efd->pclient;
+
+	i_assert(efd->callback != NULL);
+	efd->callback(efd->context, efd->input);
+
+	if (efd->input->closed || i_stream_is_eof(efd->input)) {
+		if (!program_client_input_pending(pclient))
+			program_client_disconnect(pclient, FALSE);
+	}
+}
+
+int program_client_connected
+(struct program_client *pclient)
+{
+	int ret = 1;
+
+	pclient->start_time = ioloop_time;
+	if (pclient->to != NULL)
+		timeout_remove(&pclient->to);
+	if (pclient->set.input_idle_timeout_secs != 0) {
+		pclient->to = timeout_add(pclient->set.input_idle_timeout_secs*1000,
+      program_client_timeout, pclient);
+	}
+
+	/* run output */
+	if (pclient->program_output != NULL &&
+		(ret=program_client_program_output(pclient)) == 0) {
+		if (pclient->program_output != NULL) {
+			o_stream_set_flush_callback
+				(pclient->program_output, program_client_program_output, pclient);
+		}
+	}
+
+	return ret;
+}
+
+void program_client_init
+(struct program_client *pclient, pool_t pool, const char *path,
+	const char *const *args, const struct program_client_settings *set)
+{
+	pclient->pool = pool;
+	pclient->path = p_strdup(pool, path);
+	if (args != NULL)
+		pclient->args = p_strarray_dup(pool, args);
+	pclient->set = *set;
+	pclient->debug = set->debug;
+	pclient->fd_in = -1;
+	pclient->fd_out = -1;
+}
+
+void program_client_set_input
+(struct program_client *pclient, struct istream *input)
+{
+	if (pclient->input != NULL)
+		i_stream_unref(&pclient->input);
+	if (input != NULL)
+		i_stream_ref(input);
+	pclient->input = input;
+}
+
+void program_client_set_output
+(struct program_client *pclient, struct ostream *output)
+{
+	if (pclient->output != NULL)
+		o_stream_unref(&pclient->output);
+	if (output != NULL)
+		o_stream_ref(output);
+	pclient->output = output;
+	pclient->output_seekable = FALSE;
+	i_free(pclient->temp_prefix);
+}
+
+void program_client_set_output_seekable
+(struct program_client *pclient, const char *temp_prefix)
+{
+	if (pclient->output != NULL)
+		o_stream_unref(&pclient->output);
+	pclient->temp_prefix = i_strdup(temp_prefix);
+	pclient->output_seekable = TRUE;
+}
+
+struct istream *program_client_get_output_seekable
+(struct program_client *pclient)
+{
+	struct istream *input = pclient->seekable_output;
+	
+	pclient->seekable_output = NULL;
+
+	i_stream_seek(input, 0);
+	return input;
+}
+
+#undef program_client_set_extra_fd
+void program_client_set_extra_fd
+(struct program_client *pclient, int fd,
+	program_client_fd_callback_t *callback, void *context)
+{
+	struct program_client_extra_fd *efds;
+	struct program_client_extra_fd *efd = NULL;
+	unsigned int i, count;
+	i_assert(fd > 1);
+	
+	if (!array_is_created(&pclient->extra_fds))
+		p_array_init(&pclient->extra_fds, pclient->pool, 2);
+
+	efds = array_get_modifiable(&pclient->extra_fds, &count);
+	for (i = 0; i < count; i++) {
+		if (efds[i].child_fd == fd) {
+			efd = &efds[i];
+			break;
+		}
+	}
+
+	if (efd == NULL) {
+		efd = array_append_space(&pclient->extra_fds);
+		efd->pclient = pclient;
+		efd->child_fd = fd;
+		efd->parent_fd = -1;
+	}
+	efd->callback = callback;
+	efd->context = context;
+}
+
+void program_client_set_env
+(struct program_client *pclient, const char *name, const char *value)
+{
+	const char *env;
+
+	if (!array_is_created(&pclient->envs))
+		p_array_init(&pclient->envs, pclient->pool, 16);
+
+	env = p_strdup_printf(pclient->pool, "%s=%s", name, value);
+	array_append(&pclient->envs, &env, 1);
+}
+
+void program_client_init_streams(struct program_client *pclient)
+{
+	/* Create streams for normal program I/O */
+	if (pclient->fd_out >= 0) {
+		pclient->program_output =
+			o_stream_create_fd(pclient->fd_out, MAX_OUTPUT_BUFFER_SIZE, FALSE);
+		o_stream_set_name(pclient->program_output, "program stdin");
+	}
+	if (pclient->fd_in >= 0) {
+		struct istream *input;
+		
+		input = i_stream_create_fd(pclient->fd_in, (size_t)-1, FALSE);
+
+		pclient->program_input = input;
+		i_stream_set_name(pclient->program_input, "program stdout");
+
+		pclient->io = io_add
+			(pclient->fd_in, IO_READ, program_client_program_input, pclient);
+	}
+
+	/* Create streams for additional output through side-channel fds */
+	if (array_is_created(&pclient->extra_fds)) {
+		struct program_client_extra_fd *efds = NULL;
+		unsigned int count, i;
+		
+		efds = array_get_modifiable(&pclient->extra_fds, &count);
+		for (i = 0; i < count; i++) {
+			i_assert(efds[i].parent_fd >= 0);
+			efds[i].input = i_stream_create_fd
+				(efds[i].parent_fd, (size_t)-1, FALSE);
+			i_stream_set_name(efds[i].input,
+				t_strdup_printf("program output fd=%d", efds[i].child_fd));
+			efds[i].io = io_add
+				(efds[i].parent_fd, IO_READ, program_client_extra_fd_input, &efds[i]);
+		}
+	}
+}
+
+void program_client_destroy(struct program_client **_pclient)
+{
+	struct program_client *pclient = *_pclient;
+
+	program_client_disconnect(pclient, TRUE);
+
+	if (pclient->input != NULL)
+		i_stream_unref(&pclient->input);
+	if (pclient->output != NULL)
+		o_stream_unref(&pclient->output);
+	if (pclient->seekable_output != NULL)
+		i_stream_unref(&pclient->seekable_output);
+	if (pclient->io != NULL)
+		io_remove(&pclient->io);
+	if (pclient->ioloop != NULL)
+		io_loop_destroy(&pclient->ioloop);
+	i_free(pclient->temp_prefix);
+	pool_unref(&pclient->pool);
+	*_pclient = NULL;
+}
+
+int program_client_run(struct program_client *pclient)
+{
+	int ret;
+
+	/* reset */
+	pclient->disconnected = FALSE;
+	pclient->exit_code = 1;
+	pclient->error = PROGRAM_CLIENT_ERROR_NONE;
+
+	pclient->ioloop = io_loop_create();
+
+	if ((ret=program_client_connect(pclient)) >= 0) {
+		/* run output */
+		if (ret > 0 && pclient->program_output != NULL &&
+			(ret=o_stream_flush(pclient->program_output)) == 0) {
+			o_stream_set_flush_callback
+				(pclient->program_output, program_client_program_output, pclient);
+		}
+
+		/* run i/o event loop */
+		if (ret < 0) {
+			i_error("write(%s) failed: %s",
+				o_stream_get_name(pclient->program_output),
+				o_stream_get_error(pclient->program_output));
+			pclient->error = PROGRAM_CLIENT_ERROR_IO;
+		} else if (!pclient->disconnected &&
+			(ret == 0 || program_client_input_pending(pclient))) {
+			io_loop_run(pclient->ioloop);
+		}
+
+		/* finished */
+		program_client_disconnect(pclient, FALSE);
+	}
+
+	io_loop_destroy(&pclient->ioloop);
+
+	if (pclient->error != PROGRAM_CLIENT_ERROR_NONE)
+		return -1;
+
+	return pclient->exit_code;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-program-client/program-client.h	Mon Sep 12 13:02:23 2016 +0300
@@ -0,0 +1,54 @@
+/* Copyright (c) 2002-2016 Dovecot authors, see the included COPYING file
+ */
+
+#ifndef PROGRAM_CLIENT_H
+#define PROGRAM_CLIENT_H
+
+struct program_client;
+
+struct program_client_settings {
+	unsigned int client_connect_timeout_msecs;
+	unsigned int input_idle_timeout_secs;
+
+	uid_t uid;
+	gid_t gid;
+
+	bool debug:1;
+	bool drop_stderr:1;
+};
+
+typedef void program_client_fd_callback_t(void *context, struct istream *input);
+
+struct program_client *program_client_local_create(const char *bin_path,
+	const char *const *args,
+	const struct program_client_settings *set);
+struct program_client *program_client_remote_create(const char *socket_path,
+	const char *const *args,
+	const struct program_client_settings *set, bool noreply);
+
+void program_client_destroy(struct program_client **_pclient);
+
+void program_client_set_input(struct program_client *pclient,
+	struct istream *input);
+void program_client_set_output(struct program_client *pclient,
+	struct ostream *output);
+
+void program_client_set_output_seekable(struct program_client *pclient,
+	const char *temp_prefix);
+struct istream *program_client_get_output_seekable(struct program_client *pclient);
+
+/* Program provides side-channel output through an extra fd */
+void program_client_set_extra_fd(struct program_client *pclient, int fd,
+	 program_client_fd_callback_t * callback, void *context);
+#define program_client_set_extra_fd(pclient, fd, callback, context) \
+	program_client_set_extra_fd(pclient, fd + \
+		CALLBACK_TYPECHECK(callback, \
+			void (*)(typeof(context), struct istream *input)), \
+		(program_client_fd_callback_t *)callback, context)
+
+void program_client_set_env(struct program_client *pclient,
+	const char *name, const char *value);
+
+int program_client_run(struct program_client *pclient);
+
+#endif