changeset 21372:73f614573fb6

lib-lda: Moved LMTP client to lib-smtp. This makes the LMTP client available without dependency on lib-storage. For Dovecot v2.3, the newly created lib-smtp will evolve into a full client/server SMTP implementation. That will then remove the remaining SMTP code from lib-lda.
author Stephan Bosch <stephan.bosch@dovecot.fi>
date Fri, 30 Dec 2016 16:20:12 +0100
parents bf03a476152b
children 9d3889a5fbb3
files configure.ac dovecot-config.in.in src/Makefile.am src/lib-lda/Makefile.am src/lib-lda/lmtp-client.c src/lib-lda/lmtp-client.h src/lib-smtp/Makefile.am src/lib-smtp/lmtp-client.c src/lib-smtp/lmtp-client.h src/lmtp/Makefile.am
diffstat 10 files changed, 1025 insertions(+), 1009 deletions(-) [+]
line wrap: on
line diff
--- a/configure.ac	Fri Oct 28 21:34:41 2016 +0300
+++ b/configure.ac	Fri Dec 30 16:20:12 2016 +0100
@@ -2590,7 +2590,7 @@
 dnl ** Shared libraries usage
 dnl **
 
-LIBDOVECOT_LA_LIBS='$(top_builddir)/src/lib-dict-extra/libdict_extra.la $(top_builddir)/src/lib-program-client/libprogram_client.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-program-client/libprogram_client.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-smtp/libsmtp.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'
 if test "$want_shared_libs" = "yes"; then
   LIBDOVECOT_DEPS='$(top_builddir)/src/lib-dovecot/libdovecot.la'
   LIBDOVECOT="$LIBDOVECOT_DEPS \$(MODULE_LIBS)"
@@ -2954,6 +2954,7 @@
 src/lib-dovecot/Makefile
 src/lib-sasl/Makefile
 src/lib-settings/Makefile
+src/lib-smtp/Makefile
 src/lib-ssl-iostream/Makefile
 src/lib-stats/Makefile
 src/lib-test/Makefile
--- a/dovecot-config.in.in	Fri Oct 28 21:34:41 2016 +0300
+++ b/dovecot-config.in.in	Fri Dec 30 16:20:12 2016 +0100
@@ -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 -I$(incdir)/src/lib-program-client"
+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-smtp -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	Fri Oct 28 21:34:41 2016 +0300
+++ b/src/Makefile.am	Fri Dec 30 16:20:12 2016 +0100
@@ -18,6 +18,7 @@
 	lib-http \
 	lib-fs \
 	lib-mail \
+	lib-smtp \
 	lib-imap \
 	lib-imap-storage \
 	lib-program-client
--- a/src/lib-lda/Makefile.am	Fri Oct 28 21:34:41 2016 +0300
+++ b/src/lib-lda/Makefile.am	Fri Dec 30 16:20:12 2016 +0100
@@ -5,6 +5,7 @@
 	-I$(top_srcdir)/src/lib-settings \
 	-I$(top_srcdir)/src/lib-master \
 	-I$(top_srcdir)/src/lib-dns \
+	-I$(top_srcdir)/src/lib-smtp \
 	-I$(top_srcdir)/src/lib-imap \
 	-I$(top_srcdir)/src/lib-mail \
 	-I$(top_srcdir)/src/lib-storage
@@ -12,7 +13,6 @@
 liblda_la_SOURCES = \
 	duplicate.c \
 	lda-settings.c \
-	lmtp-client.c \
 	mail-deliver.c \
 	mail-send.c \
 	smtp-client.c
@@ -20,7 +20,6 @@
 headers = \
 	duplicate.h \
 	lda-settings.h \
-	lmtp-client.h \
 	mail-deliver.h \
 	mail-send.h \
 	smtp-client.h
--- a/src/lib-lda/lmtp-client.c	Fri Oct 28 21:34:41 2016 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,898 +0,0 @@
-/* Copyright (c) 2009-2016 Dovecot authors, see the included COPYING file */
-
-#include "lib.h"
-#include "array.h"
-#include "ioloop.h"
-#include "net.h"
-#include "istream.h"
-#include "ostream.h"
-#include "str.h"
-#include "dns-lookup.h"
-#include "lmtp-client.h"
-
-#include <ctype.h>
-
-#define LMTP_MAX_LINE_LEN 1024
-#define LMTP_CLIENT_DNS_LOOKUP_TIMEOUT_MSECS (60*1000)
-
-enum lmtp_input_state {
-	LMTP_INPUT_STATE_GREET,
-	LMTP_INPUT_STATE_LHLO,
-	LMTP_INPUT_STATE_MAIL_FROM,
-	LMTP_INPUT_STATE_RCPT_TO,
-	LMTP_INPUT_STATE_DATA_CONTINUE,
-	LMTP_INPUT_STATE_DATA,
-	LMTP_INPUT_STATE_XCLIENT
-};
-
-struct lmtp_rcpt {
-	const char *address;
-	lmtp_callback_t *rcpt_to_callback;
-	lmtp_callback_t *data_callback;
-	void *context;
-
-	struct lmtp_recipient_params params;
-
-	unsigned int data_called:1;
-	unsigned int failed:1;
-};
-
-struct lmtp_client {
-	pool_t pool;
-	int refcount;
-
-	struct lmtp_client_settings set;
-	const char *host;
-	struct ip_addr ip;
-	in_port_t port;
-	enum lmtp_client_protocol protocol;
-	enum lmtp_input_state input_state;
-	const char *global_fail_string;
-	string_t *input_multiline;
-	const char **xclient_args;
-
-	struct dns_lookup *dns_lookup;
-	struct istream *input;
-	struct ostream *output;
-	struct io *io;
-	struct timeout *to;
-	int fd;
-
-	void (*data_output_callback)(void *);
-	void *data_output_context;
-
-	lmtp_finish_callback_t *finish_callback;
-	void *finish_context;
-
-	const char *data_header;
-	ARRAY(struct lmtp_rcpt) recipients;
-	unsigned int rcpt_next_receive_idx;
-	unsigned int rcpt_next_data_idx;
-	unsigned int rcpt_next_send_idx;
-	struct istream *data_input;
-	unsigned char output_last;
-	struct lmtp_client_times times;
-
-	unsigned int running:1;
-	unsigned int xclient_sent:1;
-	unsigned int rcpt_to_successes:1;
-	unsigned int output_finished:1;
-	unsigned int finish_called:1;
-	unsigned int global_remote_failure:1;
-};
-
-static void lmtp_client_send_rcpts(struct lmtp_client *client);
-
-struct lmtp_client *
-lmtp_client_init(const struct lmtp_client_settings *set,
-		 lmtp_finish_callback_t *finish_callback, void *context)
-{
-	struct lmtp_client *client;
-	pool_t pool;
-
-	i_assert(*set->mail_from == '<');
-	i_assert(*set->my_hostname != '\0');
-
-	pool = pool_alloconly_create("lmtp client", 512);
-	client = p_new(pool, struct lmtp_client, 1);
-	client->refcount = 1;
-	client->pool = pool;
-	client->set.mail_from = p_strdup(pool, set->mail_from);
-	client->set.my_hostname = p_strdup(pool, set->my_hostname);
-	client->set.dns_client_socket_path =
-		p_strdup(pool, set->dns_client_socket_path);
-	client->set.source_ip = set->source_ip;
-	client->set.source_port = set->source_port;
-	client->set.proxy_ttl = set->proxy_ttl;
-	client->set.proxy_timeout_secs = set->proxy_timeout_secs;
-	client->set.timeout_secs = set->timeout_secs;
-	client->finish_callback = finish_callback;
-	client->finish_context = context;
-	client->fd = -1;
-	client->input_multiline = str_new(default_pool, 128);
-	p_array_init(&client->recipients, pool, 16);
-	return client;
-}
-
-void lmtp_client_close(struct lmtp_client *client)
-{
-	if (client->dns_lookup != NULL)
-		dns_lookup_abort(&client->dns_lookup);
-	if (client->to != NULL)
-		timeout_remove(&client->to);
-	if (client->io != NULL)
-		io_remove(&client->io);
-	if (client->input != NULL)
-		i_stream_close(client->input);
-	if (client->output != NULL)
-		o_stream_close(client->output);
-	if (client->fd != -1) {
-		net_disconnect(client->fd);
-		client->fd = -1;
-	}
-	if (client->data_input != NULL)
-		i_stream_unref(&client->data_input);
-	client->output_finished = TRUE;
-
-	if (!client->finish_called) {
-		client->finish_called = TRUE;
-		client->finish_callback(client->finish_context);
-	}
-}
-
-static void lmtp_client_ref(struct lmtp_client *client)
-{
-	client->refcount++;
-}
-
-static void lmtp_client_unref(struct lmtp_client **_client)
-{
-	struct lmtp_client *client = *_client;
-
-	*_client = NULL;
-
-	i_assert(client->refcount > 0);
-	if (--client->refcount > 0)
-		return;
-
-	i_assert(client->finish_called);
-	if (client->input != NULL)
-		i_stream_unref(&client->input);
-	if (client->output != NULL)
-		o_stream_unref(&client->output);
-	str_free(&client->input_multiline);
-	pool_unref(&client->pool);
-}
-
-void lmtp_client_deinit(struct lmtp_client **_client)
-{
-	struct lmtp_client *client = *_client;
-
-	*_client = NULL;
-
-	lmtp_client_close(client);
-	lmtp_client_unref(&client);
-}
-
-const char *lmtp_client_state_to_string(struct lmtp_client *client)
-{
-	uoff_t size;
-
-	switch (client->input_state) {
-	case LMTP_INPUT_STATE_GREET:
-		return "greeting";
-	case LMTP_INPUT_STATE_LHLO:
-		return "LHLO";
-	case LMTP_INPUT_STATE_MAIL_FROM:
-		return "MAIL FROM";
-	case LMTP_INPUT_STATE_RCPT_TO:
-		return "RCPT TO";
-	case LMTP_INPUT_STATE_DATA_CONTINUE:
-		return "DATA init";
-	case LMTP_INPUT_STATE_DATA:
-		if (client->output_finished)
-			return "DATA reply";
-		else if (i_stream_get_size(client->data_input, FALSE, &size) > 0) {
-			return t_strdup_printf(
-				"DATA (%"PRIuUOFF_T"/%"PRIuUOFF_T")",
-				client->data_input->v_offset, size);
-		} else {
-			return t_strdup_printf("DATA (%"PRIuUOFF_T"/?)",
-					       client->data_input->v_offset);
-		}
-	case LMTP_INPUT_STATE_XCLIENT:
-		return "XCLIENT";
-	}
-	return "??";
-}
-
-static void
-lmtp_client_fail_full(struct lmtp_client *client, const char *line, bool remote)
-{
-	enum lmtp_client_result result;
-	struct lmtp_rcpt *recipients;
-	unsigned int i, count;
-
-	client->global_fail_string = p_strdup(client->pool, line);
-	client->global_remote_failure = remote;
-	result = remote ? LMTP_CLIENT_RESULT_REMOTE_ERROR :
-		LMTP_CLIENT_RESULT_INTERNAL_ERROR;
-
-	lmtp_client_ref(client);
-	recipients = array_get_modifiable(&client->recipients, &count);
-	for (i = client->rcpt_next_receive_idx; i < count; i++) {
-		recipients[i].rcpt_to_callback(result, line,
-					       recipients[i].context);
-		recipients[i].failed = TRUE;
-	}
-	client->rcpt_next_receive_idx = count;
-
-	for (i = client->rcpt_next_data_idx; i < count; i++) {
-		if (!recipients[i].failed) {
-			recipients[i].data_callback(result, line,
-						    recipients[i].context);
-		}
-	}
-	client->rcpt_next_data_idx = count;
-
-	lmtp_client_close(client);
-	lmtp_client_unref(&client);
-}
-
-static void
-lmtp_client_fail_remote(struct lmtp_client *client, const char *line)
-{
-	lmtp_client_fail_full(client, line, TRUE);
-}
-
-void lmtp_client_fail(struct lmtp_client *client, const char *line)
-{
-	lmtp_client_fail_full(client, line, FALSE);
-}
-
-static void
-lmtp_client_rcpt_next(struct lmtp_client *client, const char *line)
-{
-	struct lmtp_rcpt *rcpt;
-	enum lmtp_client_result result;
-
-	result = line[0] == '2' ? LMTP_CLIENT_RESULT_OK :
-		LMTP_CLIENT_RESULT_REMOTE_ERROR;
-	if (result == LMTP_CLIENT_RESULT_OK)
-		client->rcpt_to_successes = TRUE;
-
-	rcpt = array_idx_modifiable(&client->recipients,
-				    client->rcpt_next_receive_idx);
-	client->rcpt_next_receive_idx++;
-
-	rcpt->failed = result != LMTP_CLIENT_RESULT_OK;
-	rcpt->rcpt_to_callback(result, line, rcpt->context);
-}
-
-static int lmtp_client_send_data_cmd(struct lmtp_client *client)
-{
-	if (client->rcpt_next_receive_idx < array_count(&client->recipients))
-		return 0;
-
-	if (client->global_fail_string != NULL) {
-		lmtp_client_fail_full(client, client->global_fail_string,
-				      client->global_remote_failure);
-		return -1;
-	} else if (!client->rcpt_to_successes) {
-		/* This error string shouldn't become visible anywhere */
-		lmtp_client_fail_full(client, "No valid recipients", FALSE);
-		return -1;
-	} else {
-		client->input_state++;
-		o_stream_nsend_str(client->output, "DATA\r\n");
-		return 0;
-	}
-}
-
-static int
-lmtp_client_data_next(struct lmtp_client *client, const char *line)
-{
-	struct lmtp_rcpt *rcpt;
-	unsigned int i, count;
-	enum lmtp_client_result result;
-
-	rcpt = array_get_modifiable(&client->recipients, &count);
-	for (i = client->rcpt_next_data_idx; i < count; i++) {
-		if (rcpt[i].failed) {
-			/* already called rcpt_to_callback with failure */
-			continue;
-		}
-
-		client->rcpt_next_data_idx = i + 1;
-		rcpt[i].failed = line[0] != '2';
-		result = rcpt[i].failed ? LMTP_CLIENT_RESULT_REMOTE_ERROR :
-			LMTP_CLIENT_RESULT_OK;
-		rcpt[i].data_callback(result, line, rcpt[i].context);
-		if (client->protocol == LMTP_CLIENT_PROTOCOL_LMTP)
-			break;
-	}
-	if (client->rcpt_next_data_idx < count)
-		return 0;
-
-	o_stream_send_str(client->output, "QUIT\r\n");
-	lmtp_client_close(client);
-	return -1;
-}
-
-static int lmtp_client_send_data(struct lmtp_client *client)
-{
-	const unsigned char *data;
-	unsigned char add;
-	size_t i, size;
-	bool sent_bytes = FALSE;
-	int ret;
-
-	if (client->output_finished)
-		return 0;
-
-	while ((ret = i_stream_read_data(client->data_input,
-					 &data, &size, 0)) > 0) {
-		add = '\0';
-		for (i = 0; i < size; i++) {
-			if (data[i] == '\n') {
-				if ((i == 0 && client->output_last != '\r') ||
-				    (i > 0 && data[i-1] != '\r')) {
-					/* missing CR */
-					add = '\r';
-					break;
-				}
-			} else if (data[i] == '.' &&
-				   ((i == 0 && client->output_last == '\n') ||
-				    (i > 0 && data[i-1] == '\n'))) {
-				/* escape the dot */
-				add = '.';
-				break;
-			}
-		}
-
-		if (i > 0) {
-			if (o_stream_send(client->output, data, i) < 0)
-				break;
-			client->output_last = data[i-1];
-			i_stream_skip(client->data_input, i);
-			sent_bytes = TRUE;
-		}
-
-		if (o_stream_get_buffer_used_size(client->output) >= 4096) {
-			if ((ret = o_stream_flush(client->output)) < 0)
-				break;
-			if (ret == 0) {
-				/* continue later */
-				o_stream_set_flush_pending(client->output, TRUE);
-				return 0;
-			}
-		}
-
-		if (add != '\0') {
-			if (o_stream_send(client->output, &add, 1) < 0)
-				break;
-
-			client->output_last = add;
-		}
-	}
-	if (client->data_input->stream_errno != 0) {
-		i_error("lmtp client: read(%s) failed: %s",
-			i_stream_get_name(client->data_input),
-			i_stream_get_error(client->data_input));
-		lmtp_client_fail(client,
-			"451 4.3.0 Internal failure while reading DATA input");
-		return -1;
-	}
-	if (sent_bytes && client->data_output_callback != NULL)
-		client->data_output_callback(client->data_output_context);
-
-	if (ret == 0 || ret == -2) {
-		/* -2 can happen with tee istreams */
-		return 0;
-	}
-
-	if (client->output_last != '\n') {
-		/* didn't end with CRLF */
-		o_stream_nsend(client->output, "\r\n", 2);
-	}
-	o_stream_nsend(client->output, ".\r\n", 3);
-	client->output_finished = TRUE;
-	io_loop_time_refresh();
-	client->times.data_sent = ioloop_timeval;
-	return 0;
-}
-
-static void lmtp_client_send_handshake(struct lmtp_client *client)
-{
-	switch (client->protocol) {
-	case LMTP_CLIENT_PROTOCOL_LMTP:
-		o_stream_nsend_str(client->output,
-			t_strdup_printf("LHLO %s\r\n",
-					client->set.my_hostname));
-		break;
-	case LMTP_CLIENT_PROTOCOL_SMTP:
-		o_stream_nsend_str(client->output,
-			t_strdup_printf("EHLO %s\r\n",
-					client->set.my_hostname));
-		break;
-	}
-}
-
-static int lmtp_input_get_reply_code(const char *line, int *reply_code_r,
-				     string_t *multiline)
-{
-	if (!i_isdigit(line[0]) || !i_isdigit(line[1]) || !i_isdigit(line[2]))
-		return -1;
-
-	*reply_code_r = (line[0]-'0') * 100 +
-		(line[1]-'0') * 10 +
-		(line[2]-'0');
-
-	if (line[3] == ' ') {
-		/* final reply */
-		return 1;
-	} else if (line[3] == '-') {
-		/* multiline reply. */
-		str_append(multiline, line);
-		str_append_c(multiline, '\n');
-		return 0;
-	} else {
-		/* invalid input */
-		return -1;
-	}
-}
-
-static void
-lmtp_client_parse_capabilities(struct lmtp_client *client, const char *lines)
-{
-	const char *const *linep;
-
-	for (linep = t_strsplit(lines, "\n"); *linep != NULL; linep++) {
-		const char *line = *linep;
-
-		line += 4; /* already checked this is valid */
-		if (strncasecmp(line, "XCLIENT ", 8) == 0) {
-			client->xclient_args =
-				(void *)p_strsplit(client->pool, line + 8, " ");
-		}
-	}
-}
-
-static bool lmtp_client_send_xclient(struct lmtp_client *client)
-{
-	string_t *str;
-	size_t empty_len;
-
-	if (client->xclient_args == NULL) {
-		/* not supported */
-		return FALSE;
-	}
-	if (client->xclient_sent)
-		return FALSE;
-
-	str = t_str_new(64);
-	str_append(str, "XCLIENT");
-	empty_len = str_len(str);
-	if (client->set.source_ip.family != 0 &&
-	    str_array_icase_find(client->xclient_args, "ADDR"))
-		str_printfa(str, " ADDR=%s", net_ip2addr(&client->set.source_ip));
-	if (client->set.source_port != 0 &&
-	    str_array_icase_find(client->xclient_args, "PORT"))
-		str_printfa(str, " PORT=%u", client->set.source_port);
-	if (client->set.proxy_ttl != 0 &&
-	    str_array_icase_find(client->xclient_args, "TTL"))
-		str_printfa(str, " TTL=%u", client->set.proxy_ttl);
-	if (client->set.proxy_timeout_secs != 0 &&
-	    str_array_icase_find(client->xclient_args, "TIMEOUT"))
-		str_printfa(str, " TIMEOUT=%u", client->set.proxy_timeout_secs);
-
-	if (str_len(str) == empty_len)
-		return FALSE;
-
-	str_append(str, "\r\n");
-	o_stream_nsend(client->output, str_data(str), str_len(str));
-	return TRUE;
-}
-
-static int lmtp_client_input_line(struct lmtp_client *client, const char *line)
-{
-	int ret, reply_code = 0;
-
-	if ((ret = lmtp_input_get_reply_code(line, &reply_code,
-					     client->input_multiline)) <= 0) {
-		if (ret == 0)
-			return 0;
-		lmtp_client_fail(client, t_strdup_printf(
-			"451 4.5.0 Received invalid input: %s", line));
-		return -1;
-	}
-
-	switch (client->input_state) {
-	case LMTP_INPUT_STATE_GREET:
-		if (reply_code != 220) {
-			lmtp_client_fail(client, t_strdup_printf(
-				"451 4.5.0 Received invalid greeting: %s", line));
-			return -1;
-		}
-		client->times.banner_received = ioloop_timeval;
-		lmtp_client_send_handshake(client);
-		client->input_state = LMTP_INPUT_STATE_LHLO;
-		break;
-	case LMTP_INPUT_STATE_XCLIENT:
-		if (reply_code != 220) {
-			lmtp_client_fail(client, t_strdup_printf(
-				"451 4.5.0 XCLIENT failed: %s", line));
-			return -1;
-		}
-		lmtp_client_send_handshake(client);
-		client->input_state = LMTP_INPUT_STATE_LHLO;
-		break;
-	case LMTP_INPUT_STATE_LHLO:
-		if (reply_code != 250) {
-			lmtp_client_fail(client, t_strdup_printf(
-				"451 4.5.0 LHLO failed: %s", line));
-			lmtp_client_fail(client, line);
-			return -1;
-		}
-		str_append(client->input_multiline, line);
-		lmtp_client_parse_capabilities(client,
-			str_c(client->input_multiline));
-		if (lmtp_client_send_xclient(client)) {
-			client->input_state = LMTP_INPUT_STATE_XCLIENT;
-			client->xclient_sent = TRUE;
-			break;
-		}
-		o_stream_nsend_str(client->output, t_strdup_printf(
-			"MAIL FROM:%s\r\n", client->set.mail_from));
-		client->input_state++;
-		break;
-	case LMTP_INPUT_STATE_MAIL_FROM:
-		if (reply_code != 250) {
-			lmtp_client_fail(client, t_strdup_printf(
-				"451 4.5.0 MAIL FROM failed: %s", line));
-			return -1;
-		}
-		client->input_state++;
-		lmtp_client_send_rcpts(client);
-		break;
-	case LMTP_INPUT_STATE_RCPT_TO:
-		lmtp_client_rcpt_next(client, line);
-		if (client->data_input == NULL)
-			break;
-		if (lmtp_client_send_data_cmd(client) < 0)
-			return -1;
-		break;
-	case LMTP_INPUT_STATE_DATA_CONTINUE:
-		/* Start sending DATA */
-		if (strncmp(line, "354", 3) != 0) {
-			lmtp_client_fail_remote(client, line);
-			return -1;
-		}
-		client->input_state++;
-		client->times.data_started = ioloop_timeval;
-		if (client->data_header != NULL)
-			o_stream_nsend_str(client->output, client->data_header);
-		if (lmtp_client_send_data(client) < 0)
-			return -1;
-		break;
-	case LMTP_INPUT_STATE_DATA:
-		/* DATA replies */
-		if (lmtp_client_data_next(client, line) < 0)
-			return -1;
-		break;
-	}
-	return 1;
-}
-
-static void lmtp_client_input(struct lmtp_client *client)
-{
-	const char *line;
-	int ret;
-
-	lmtp_client_ref(client);
-	o_stream_cork(client->output);
-	while ((line = i_stream_read_next_line(client->input)) != NULL) {
-		T_BEGIN {
-			ret = lmtp_client_input_line(client, line);
-		} T_END;
-		if (ret < 0) {
-			o_stream_uncork(client->output);
-			lmtp_client_unref(&client);
-			return;
-		}
-		if (ret > 0)
-			str_truncate(client->input_multiline, 0);
-	}
-
-	if (client->input->stream_errno == ENOBUFS) {
-		lmtp_client_fail(client,
-				 "501 5.5.4 Command reply line too long");
-	} else if (client->input->stream_errno != 0) {
-		i_error("lmtp client: read() failed: %s",
-			i_stream_get_error(client->input));
-		lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
-				 " (read failure)");
-	} else if (client->input->eof) {
-		lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
-				 " (disconnected in input)");
-	}
-	o_stream_uncork(client->output);
-	if (client->to != NULL)
-		timeout_reset(client->to);
-	lmtp_client_unref(&client);
-}
-
-static void lmtp_client_wait_connect(struct lmtp_client *client)
-{
-	int err;
-
-	err = net_geterror(client->fd);
-	if (err != 0) {
-		i_error("lmtp client: connect(%s, %u) failed: %s",
-			client->host, client->port, strerror(err));
-		lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
-				 " (connect)");
-		return;
-	}
-	if (client->to != NULL)
-		timeout_remove(&client->to);
-	io_remove(&client->io);
-	client->io = io_add(client->fd, IO_READ, lmtp_client_input, client);
-	lmtp_client_input(client);
-}
-
-static void lmtp_client_connect_timeout(struct lmtp_client *client)
-{
-	i_error("lmtp client: connect(%s, %u) failed: Timed out in %u secs",
-		client->host, client->port, client->set.timeout_secs);
-	lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
-			 " (connect timeout)");
-}
-
-static int lmtp_client_output(struct lmtp_client *client)
-{
-	int ret;
-
-	lmtp_client_ref(client);
-	o_stream_cork(client->output);
-	if ((ret = o_stream_flush(client->output)) < 0)
-		lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
-				 " (disconnected in output)");
-	else if (client->input_state == LMTP_INPUT_STATE_DATA)
-		(void)lmtp_client_send_data(client);
-	o_stream_uncork(client->output);
-	if (client->to != NULL)
-		timeout_reset(client->to);
-	lmtp_client_unref(&client);
-	return ret;
-}
-
-static int lmtp_client_connect(struct lmtp_client *client)
-{
-	i_assert(client->fd == -1);
-
-	client->times.connect_started = ioloop_timeval;
-
-	client->fd = net_connect_ip(&client->ip, client->port, NULL);
-	if (client->fd == -1) {
-		i_error("lmtp client: connect(%s, %u) failed: %m",
-			client->host, client->port);
-		return -1;
-	}
-	client->input =
-		i_stream_create_fd(client->fd, LMTP_MAX_LINE_LEN, FALSE);
-	client->output = o_stream_create_fd(client->fd, (size_t)-1, FALSE);
-	o_stream_set_no_error_handling(client->output, TRUE);
-	o_stream_set_flush_callback(client->output, lmtp_client_output, client);
-	/* we're already sending data in ostream, so can't use IO_WRITE here */
-	client->io = io_add(client->fd, IO_READ,
-			    lmtp_client_wait_connect, client);
-	if (client->set.timeout_secs > 0) {
-		client->to = timeout_add(client->set.timeout_secs*1000,
-					 lmtp_client_connect_timeout, client);
-	}
-	return 0;
-}
-
-static void lmtp_client_dns_done(const struct dns_lookup_result *result,
-				 struct lmtp_client *client)
-{
-	client->dns_lookup = NULL;
-
-	if (result->ret != 0) {
-		i_error("lmtp client: DNS lookup of %s failed: %s",
-			client->host, result->error);
-		if (client->running) {
-			lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
-					 " (DNS lookup)");
-		}
-	} else {
-		client->ip = result->ips[0];
-		if (lmtp_client_connect(client) < 0 && client->running) {
-			lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
-					 " (connect)");
-		}
-	}
-}
-
-int lmtp_client_connect_tcp(struct lmtp_client *client,
-			    enum lmtp_client_protocol protocol,
-			    const char *host, in_port_t port)
-{
-	struct dns_lookup_settings dns_lookup_set;
-	struct ip_addr *ips;
-	unsigned int ips_count;
-	int ret;
-
-	client->input_state = LMTP_INPUT_STATE_GREET;
-	client->host = p_strdup(client->pool, host);
-	client->port = port;
-	client->protocol = protocol;
-
-	if (*host == '\0') {
-		i_error("lmtp client: host not given");
-		return -1;
-	}
-
-	memset(&dns_lookup_set, 0, sizeof(dns_lookup_set));
-	dns_lookup_set.dns_client_socket_path =
-		client->set.dns_client_socket_path;
-	dns_lookup_set.timeout_msecs = LMTP_CLIENT_DNS_LOOKUP_TIMEOUT_MSECS;
-
-	if (net_addr2ip(host, &client->ip) == 0) {
-		/* IP address */
-	} else if (dns_lookup_set.dns_client_socket_path == NULL) {
-		/* no dns-client, use blocking lookup */
-		ret = net_gethostbyname(host, &ips, &ips_count);
-		if (ret != 0) {
-			i_error("lmtp client: DNS lookup of %s failed: %s",
-				client->host, net_gethosterror(ret));
-			return -1;
-		}
-		client->ip = ips[0];
-	} else {
-		if (dns_lookup(host, &dns_lookup_set,
-			       lmtp_client_dns_done, client,
-			       &client->dns_lookup) < 0)
-			return -1;
-		client->running = TRUE;
-		return 0;
-	}
-
-	if (lmtp_client_connect(client) < 0)
-		return -1;
-	return 0;
-}
-
-void lmtp_client_set_data_header(struct lmtp_client *client, const char *str)
-{
-	client->data_header = p_strdup(client->pool, str);
-}
-
-static void lmtp_append_xtext(string_t *dest, const char *str)
-{
-	unsigned int i;
-
-	for (i = 0; str[i] != '\0'; i++) {
-		if (str[i] >= 33 && str[i] <= 126 &&
-		    str[i] != '+' && str[i] != '=')
-			str_append_c(dest, str[i]);
-		else
-			str_printfa(dest, "+%02X", str[i]);
-	}
-}
-
-static void lmtp_client_send_rcpts(struct lmtp_client *client)
-{
-	const struct lmtp_rcpt *rcpt;
-	unsigned int i, count;
-	string_t *str = t_str_new(128);
-
-	rcpt = array_get(&client->recipients, &count);
-	for (i = client->rcpt_next_send_idx; i < count; i++) {
-		str_truncate(str, 0);
-		str_printfa(str, "RCPT TO:<%s>", rcpt[i].address);
-		if (rcpt->params.dsn_orcpt != NULL) {
-			str_append(str, " ORCPT=");
-			lmtp_append_xtext(str, rcpt->params.dsn_orcpt);
-		}
-		str_append(str, "\r\n");
-		o_stream_nsend(client->output, str_data(str), str_len(str));
-	}
-	client->rcpt_next_send_idx = i;
-}
-
-void lmtp_client_add_rcpt(struct lmtp_client *client, const char *address,
-			  lmtp_callback_t *rcpt_to_callback,
-			  lmtp_callback_t *data_callback, void *context)
-{
-	struct lmtp_recipient_params params;
-
-	memset(&params, 0, sizeof(params));
-	lmtp_client_add_rcpt_params(client, address, &params, rcpt_to_callback,
-				    data_callback, context);
-}
-
-void lmtp_client_add_rcpt_params(struct lmtp_client *client, const char *address,
-				 const struct lmtp_recipient_params *params,
-				 lmtp_callback_t *rcpt_to_callback,
-				 lmtp_callback_t *data_callback, void *context)
-{
-	struct lmtp_rcpt *rcpt;
-	enum lmtp_client_result result;
-
-	rcpt = array_append_space(&client->recipients);
-	rcpt->address = p_strdup(client->pool, address);
-	rcpt->params.dsn_orcpt = p_strdup(client->pool, params->dsn_orcpt);
-	rcpt->rcpt_to_callback = rcpt_to_callback;
-	rcpt->data_callback = data_callback;
-	rcpt->context = context;
-
-	if (client->global_fail_string != NULL) {
-		/* we've already failed */
-		client->rcpt_next_receive_idx++;
-		i_assert(client->rcpt_next_receive_idx ==
-			 array_count(&client->recipients));
-
-		result = client->global_remote_failure ?
-			LMTP_CLIENT_RESULT_REMOTE_ERROR :
-			LMTP_CLIENT_RESULT_INTERNAL_ERROR;
-		rcpt->failed = TRUE;
-		rcpt_to_callback(result, client->global_fail_string, context);
-	} else if (client->input_state == LMTP_INPUT_STATE_RCPT_TO)
-		lmtp_client_send_rcpts(client);
-}
-
-static void lmtp_client_timeout(struct lmtp_client *client)
-{
-	const char *line;
-
-	line = t_strdup_printf(ERRSTR_TEMP_REMOTE_FAILURE
-		" (Timed out after %u secs while waiting for reply to %s)",
-		client->set.timeout_secs, lmtp_client_state_to_string(client));
-	lmtp_client_fail(client, line);
-}
-
-void lmtp_client_send(struct lmtp_client *client, struct istream *data_input)
-{
-	i_assert(client->data_input == NULL);
-
-	i_stream_ref(data_input);
-	client->data_input = data_input;
-
-	/* now we actually want to start doing I/O. start the timeout
-	   handling. */
-	if (client->set.timeout_secs > 0) {
-		if (client->to != NULL) {
-			/* still waiting for connect to finish */
-			timeout_remove(&client->to);
-		}
-		client->to = timeout_add(client->set.timeout_secs*1000,
-					 lmtp_client_timeout, client);
-	}
-
-	(void)lmtp_client_send_data_cmd(client);
-}
-
-void lmtp_client_send_more(struct lmtp_client *client)
-{
-	if (client->input_state == LMTP_INPUT_STATE_DATA) {
-		o_stream_cork(client->output);
-		(void)lmtp_client_send_data(client);
-		o_stream_uncork(client->output);
-	}
-}
-
-void lmtp_client_set_data_output_callback(struct lmtp_client *client,
-					  void (*callback)(void *),
-					  void *context)
-{
-	client->data_output_callback = callback;
-	client->data_output_context = context;
-}
-
-const struct lmtp_client_times *
-lmtp_client_get_times(struct lmtp_client *client)
-{
-	return &client->times;
-}
--- a/src/lib-lda/lmtp-client.h	Fri Oct 28 21:34:41 2016 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,107 +0,0 @@
-#ifndef LMTP_CLIENT_H
-#define LMTP_CLIENT_H
-
-#include "net.h"
-
-#define ERRSTR_TEMP_REMOTE_FAILURE "451 4.4.0 Remote server not answering"
-
-/* LMTP/SMTP client code. */
-
-enum lmtp_client_protocol {
-	LMTP_CLIENT_PROTOCOL_LMTP,
-	LMTP_CLIENT_PROTOCOL_SMTP
-};
-
-enum lmtp_client_result {
-	/* Command succeeded */
-	LMTP_CLIENT_RESULT_OK = 1,
-	/* Command failed because remote server returned an error */
-	LMTP_CLIENT_RESULT_REMOTE_ERROR = 0,
-	/* Command failed because of an internal error (e.g. couldn't connect
-	   to remote) */
-	LMTP_CLIENT_RESULT_INTERNAL_ERROR = -1
-};
-
-struct lmtp_recipient_params {
-	const char *dsn_orcpt;
-};
-
-struct lmtp_client_times {
-	struct timeval connect_started;
-	struct timeval banner_received;
-	struct timeval data_started;
-	struct timeval data_sent;
-};
-
-struct lmtp_client_settings {
-	const char *my_hostname;
-	/* The whole MAIL FROM line, including parameters */
-	const char *mail_from;
-	const char *dns_client_socket_path;
-
-	/* if remote server supports XCLIENT capability,
-	   send the these as ADDR/PORT/TTL/TIMEOUT */
-	struct ip_addr source_ip;
-	in_port_t source_port;
-	/* send TTL as this (default 0 means "don't send it") */
-	unsigned int proxy_ttl;
-	/* remote is notified that the connection is going to be closed after
-	   this many seconds, so it should try to keep lock waits and such
-	   lower than this. */
-	unsigned int proxy_timeout_secs;
-	/* Don't wait an answer from destination server longer than this many
-	   seconds (0 = unlimited) */
-	unsigned int timeout_secs;
-};
-
-/* reply contains the reply coming from remote server, or NULL
-   if it's a connection error. */
-typedef void lmtp_callback_t(enum lmtp_client_result result,
-			     const char *reply, void *context);
-/* called when session is finished, either because all RCPT TOs failed or
-   because all DATA replies have been received. */
-typedef void lmtp_finish_callback_t(void *context);
-
-struct lmtp_client *
-lmtp_client_init(const struct lmtp_client_settings *set,
-		 lmtp_finish_callback_t *finish_callback, void *context);
-void lmtp_client_deinit(struct lmtp_client **client);
-
-int lmtp_client_connect_tcp(struct lmtp_client *client,
-			    enum lmtp_client_protocol protocol,
-			    const char *host, in_port_t port);
-void lmtp_client_close(struct lmtp_client *client);
-
-/* Add headers from given string before the rest of the data. The string must
-   use CRLF line feeds and end with CRLF. */
-void lmtp_client_set_data_header(struct lmtp_client *client, const char *str);
-/* Add recipient to the session. rcpt_to_callback is called once LMTP server
-   replies with RCPT TO. If RCPT TO was a succees, data_callback is called
-   when DATA replies. */
-void lmtp_client_add_rcpt(struct lmtp_client *client, const char *address,
-			  lmtp_callback_t *rcpt_to_callback,
-			  lmtp_callback_t *data_callback, void *context);
-void lmtp_client_add_rcpt_params(struct lmtp_client *client, const char *address,
-				 const struct lmtp_recipient_params *params,
-				 lmtp_callback_t *rcpt_to_callback,
-				 lmtp_callback_t *data_callback, void *context);
-/* Start sending input stream as DATA. */
-void lmtp_client_send(struct lmtp_client *client, struct istream *data_input);
-/* Call this function whenever input stream can potentially be read forward.
-   This is useful with non-blocking istreams and tee-istreams. */
-void lmtp_client_send_more(struct lmtp_client *client);
-/* Fail the connection with line as the reply to unfinished RCPT TO/DATA
-   replies. This will be treated as an internal failure. */
-void lmtp_client_fail(struct lmtp_client *client, const char *line);
-/* Return the state (command reply) the client is currently waiting for. */
-const char *lmtp_client_state_to_string(struct lmtp_client *client);
-/* Call the given callback whenever client manages to send some more DATA
-   output to client. */
-void lmtp_client_set_data_output_callback(struct lmtp_client *client,
-					  void (*callback)(void *),
-					  void *context);
-/* Return LMTP client statistics. */
-const struct lmtp_client_times *
-lmtp_client_get_times(struct lmtp_client *client);
-
-#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-smtp/Makefile.am	Fri Dec 30 16:20:12 2016 +0100
@@ -0,0 +1,14 @@
+noinst_LTLIBRARIES = libsmtp.la
+
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/src/lib \
+	-I$(top_srcdir)/src/lib-dns
+
+libsmtp_la_SOURCES = \
+	lmtp-client.c
+
+headers = \
+	lmtp-client.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-smtp/lmtp-client.c	Fri Dec 30 16:20:12 2016 +0100
@@ -0,0 +1,898 @@
+/* Copyright (c) 2009-2016 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "dns-lookup.h"
+#include "lmtp-client.h"
+
+#include <ctype.h>
+
+#define LMTP_MAX_LINE_LEN 1024
+#define LMTP_CLIENT_DNS_LOOKUP_TIMEOUT_MSECS (60*1000)
+
+enum lmtp_input_state {
+	LMTP_INPUT_STATE_GREET,
+	LMTP_INPUT_STATE_LHLO,
+	LMTP_INPUT_STATE_MAIL_FROM,
+	LMTP_INPUT_STATE_RCPT_TO,
+	LMTP_INPUT_STATE_DATA_CONTINUE,
+	LMTP_INPUT_STATE_DATA,
+	LMTP_INPUT_STATE_XCLIENT
+};
+
+struct lmtp_rcpt {
+	const char *address;
+	lmtp_callback_t *rcpt_to_callback;
+	lmtp_callback_t *data_callback;
+	void *context;
+
+	struct lmtp_recipient_params params;
+
+	unsigned int data_called:1;
+	unsigned int failed:1;
+};
+
+struct lmtp_client {
+	pool_t pool;
+	int refcount;
+
+	struct lmtp_client_settings set;
+	const char *host;
+	struct ip_addr ip;
+	in_port_t port;
+	enum lmtp_client_protocol protocol;
+	enum lmtp_input_state input_state;
+	const char *global_fail_string;
+	string_t *input_multiline;
+	const char **xclient_args;
+
+	struct dns_lookup *dns_lookup;
+	struct istream *input;
+	struct ostream *output;
+	struct io *io;
+	struct timeout *to;
+	int fd;
+
+	void (*data_output_callback)(void *);
+	void *data_output_context;
+
+	lmtp_finish_callback_t *finish_callback;
+	void *finish_context;
+
+	const char *data_header;
+	ARRAY(struct lmtp_rcpt) recipients;
+	unsigned int rcpt_next_receive_idx;
+	unsigned int rcpt_next_data_idx;
+	unsigned int rcpt_next_send_idx;
+	struct istream *data_input;
+	unsigned char output_last;
+	struct lmtp_client_times times;
+
+	unsigned int running:1;
+	unsigned int xclient_sent:1;
+	unsigned int rcpt_to_successes:1;
+	unsigned int output_finished:1;
+	unsigned int finish_called:1;
+	unsigned int global_remote_failure:1;
+};
+
+static void lmtp_client_send_rcpts(struct lmtp_client *client);
+
+struct lmtp_client *
+lmtp_client_init(const struct lmtp_client_settings *set,
+		 lmtp_finish_callback_t *finish_callback, void *context)
+{
+	struct lmtp_client *client;
+	pool_t pool;
+
+	i_assert(*set->mail_from == '<');
+	i_assert(*set->my_hostname != '\0');
+
+	pool = pool_alloconly_create("lmtp client", 512);
+	client = p_new(pool, struct lmtp_client, 1);
+	client->refcount = 1;
+	client->pool = pool;
+	client->set.mail_from = p_strdup(pool, set->mail_from);
+	client->set.my_hostname = p_strdup(pool, set->my_hostname);
+	client->set.dns_client_socket_path =
+		p_strdup(pool, set->dns_client_socket_path);
+	client->set.source_ip = set->source_ip;
+	client->set.source_port = set->source_port;
+	client->set.proxy_ttl = set->proxy_ttl;
+	client->set.proxy_timeout_secs = set->proxy_timeout_secs;
+	client->set.timeout_secs = set->timeout_secs;
+	client->finish_callback = finish_callback;
+	client->finish_context = context;
+	client->fd = -1;
+	client->input_multiline = str_new(default_pool, 128);
+	p_array_init(&client->recipients, pool, 16);
+	return client;
+}
+
+void lmtp_client_close(struct lmtp_client *client)
+{
+	if (client->dns_lookup != NULL)
+		dns_lookup_abort(&client->dns_lookup);
+	if (client->to != NULL)
+		timeout_remove(&client->to);
+	if (client->io != NULL)
+		io_remove(&client->io);
+	if (client->input != NULL)
+		i_stream_close(client->input);
+	if (client->output != NULL)
+		o_stream_close(client->output);
+	if (client->fd != -1) {
+		net_disconnect(client->fd);
+		client->fd = -1;
+	}
+	if (client->data_input != NULL)
+		i_stream_unref(&client->data_input);
+	client->output_finished = TRUE;
+
+	if (!client->finish_called) {
+		client->finish_called = TRUE;
+		client->finish_callback(client->finish_context);
+	}
+}
+
+static void lmtp_client_ref(struct lmtp_client *client)
+{
+	client->refcount++;
+}
+
+static void lmtp_client_unref(struct lmtp_client **_client)
+{
+	struct lmtp_client *client = *_client;
+
+	*_client = NULL;
+
+	i_assert(client->refcount > 0);
+	if (--client->refcount > 0)
+		return;
+
+	i_assert(client->finish_called);
+	if (client->input != NULL)
+		i_stream_unref(&client->input);
+	if (client->output != NULL)
+		o_stream_unref(&client->output);
+	str_free(&client->input_multiline);
+	pool_unref(&client->pool);
+}
+
+void lmtp_client_deinit(struct lmtp_client **_client)
+{
+	struct lmtp_client *client = *_client;
+
+	*_client = NULL;
+
+	lmtp_client_close(client);
+	lmtp_client_unref(&client);
+}
+
+const char *lmtp_client_state_to_string(struct lmtp_client *client)
+{
+	uoff_t size;
+
+	switch (client->input_state) {
+	case LMTP_INPUT_STATE_GREET:
+		return "greeting";
+	case LMTP_INPUT_STATE_LHLO:
+		return "LHLO";
+	case LMTP_INPUT_STATE_MAIL_FROM:
+		return "MAIL FROM";
+	case LMTP_INPUT_STATE_RCPT_TO:
+		return "RCPT TO";
+	case LMTP_INPUT_STATE_DATA_CONTINUE:
+		return "DATA init";
+	case LMTP_INPUT_STATE_DATA:
+		if (client->output_finished)
+			return "DATA reply";
+		else if (i_stream_get_size(client->data_input, FALSE, &size) > 0) {
+			return t_strdup_printf(
+				"DATA (%"PRIuUOFF_T"/%"PRIuUOFF_T")",
+				client->data_input->v_offset, size);
+		} else {
+			return t_strdup_printf("DATA (%"PRIuUOFF_T"/?)",
+					       client->data_input->v_offset);
+		}
+	case LMTP_INPUT_STATE_XCLIENT:
+		return "XCLIENT";
+	}
+	return "??";
+}
+
+static void
+lmtp_client_fail_full(struct lmtp_client *client, const char *line, bool remote)
+{
+	enum lmtp_client_result result;
+	struct lmtp_rcpt *recipients;
+	unsigned int i, count;
+
+	client->global_fail_string = p_strdup(client->pool, line);
+	client->global_remote_failure = remote;
+	result = remote ? LMTP_CLIENT_RESULT_REMOTE_ERROR :
+		LMTP_CLIENT_RESULT_INTERNAL_ERROR;
+
+	lmtp_client_ref(client);
+	recipients = array_get_modifiable(&client->recipients, &count);
+	for (i = client->rcpt_next_receive_idx; i < count; i++) {
+		recipients[i].rcpt_to_callback(result, line,
+					       recipients[i].context);
+		recipients[i].failed = TRUE;
+	}
+	client->rcpt_next_receive_idx = count;
+
+	for (i = client->rcpt_next_data_idx; i < count; i++) {
+		if (!recipients[i].failed) {
+			recipients[i].data_callback(result, line,
+						    recipients[i].context);
+		}
+	}
+	client->rcpt_next_data_idx = count;
+
+	lmtp_client_close(client);
+	lmtp_client_unref(&client);
+}
+
+static void
+lmtp_client_fail_remote(struct lmtp_client *client, const char *line)
+{
+	lmtp_client_fail_full(client, line, TRUE);
+}
+
+void lmtp_client_fail(struct lmtp_client *client, const char *line)
+{
+	lmtp_client_fail_full(client, line, FALSE);
+}
+
+static void
+lmtp_client_rcpt_next(struct lmtp_client *client, const char *line)
+{
+	struct lmtp_rcpt *rcpt;
+	enum lmtp_client_result result;
+
+	result = line[0] == '2' ? LMTP_CLIENT_RESULT_OK :
+		LMTP_CLIENT_RESULT_REMOTE_ERROR;
+	if (result == LMTP_CLIENT_RESULT_OK)
+		client->rcpt_to_successes = TRUE;
+
+	rcpt = array_idx_modifiable(&client->recipients,
+				    client->rcpt_next_receive_idx);
+	client->rcpt_next_receive_idx++;
+
+	rcpt->failed = result != LMTP_CLIENT_RESULT_OK;
+	rcpt->rcpt_to_callback(result, line, rcpt->context);
+}
+
+static int lmtp_client_send_data_cmd(struct lmtp_client *client)
+{
+	if (client->rcpt_next_receive_idx < array_count(&client->recipients))
+		return 0;
+
+	if (client->global_fail_string != NULL) {
+		lmtp_client_fail_full(client, client->global_fail_string,
+				      client->global_remote_failure);
+		return -1;
+	} else if (!client->rcpt_to_successes) {
+		/* This error string shouldn't become visible anywhere */
+		lmtp_client_fail_full(client, "No valid recipients", FALSE);
+		return -1;
+	} else {
+		client->input_state++;
+		o_stream_nsend_str(client->output, "DATA\r\n");
+		return 0;
+	}
+}
+
+static int
+lmtp_client_data_next(struct lmtp_client *client, const char *line)
+{
+	struct lmtp_rcpt *rcpt;
+	unsigned int i, count;
+	enum lmtp_client_result result;
+
+	rcpt = array_get_modifiable(&client->recipients, &count);
+	for (i = client->rcpt_next_data_idx; i < count; i++) {
+		if (rcpt[i].failed) {
+			/* already called rcpt_to_callback with failure */
+			continue;
+		}
+
+		client->rcpt_next_data_idx = i + 1;
+		rcpt[i].failed = line[0] != '2';
+		result = rcpt[i].failed ? LMTP_CLIENT_RESULT_REMOTE_ERROR :
+			LMTP_CLIENT_RESULT_OK;
+		rcpt[i].data_callback(result, line, rcpt[i].context);
+		if (client->protocol == LMTP_CLIENT_PROTOCOL_LMTP)
+			break;
+	}
+	if (client->rcpt_next_data_idx < count)
+		return 0;
+
+	o_stream_send_str(client->output, "QUIT\r\n");
+	lmtp_client_close(client);
+	return -1;
+}
+
+static int lmtp_client_send_data(struct lmtp_client *client)
+{
+	const unsigned char *data;
+	unsigned char add;
+	size_t i, size;
+	bool sent_bytes = FALSE;
+	int ret;
+
+	if (client->output_finished)
+		return 0;
+
+	while ((ret = i_stream_read_data(client->data_input,
+					 &data, &size, 0)) > 0) {
+		add = '\0';
+		for (i = 0; i < size; i++) {
+			if (data[i] == '\n') {
+				if ((i == 0 && client->output_last != '\r') ||
+				    (i > 0 && data[i-1] != '\r')) {
+					/* missing CR */
+					add = '\r';
+					break;
+				}
+			} else if (data[i] == '.' &&
+				   ((i == 0 && client->output_last == '\n') ||
+				    (i > 0 && data[i-1] == '\n'))) {
+				/* escape the dot */
+				add = '.';
+				break;
+			}
+		}
+
+		if (i > 0) {
+			if (o_stream_send(client->output, data, i) < 0)
+				break;
+			client->output_last = data[i-1];
+			i_stream_skip(client->data_input, i);
+			sent_bytes = TRUE;
+		}
+
+		if (o_stream_get_buffer_used_size(client->output) >= 4096) {
+			if ((ret = o_stream_flush(client->output)) < 0)
+				break;
+			if (ret == 0) {
+				/* continue later */
+				o_stream_set_flush_pending(client->output, TRUE);
+				return 0;
+			}
+		}
+
+		if (add != '\0') {
+			if (o_stream_send(client->output, &add, 1) < 0)
+				break;
+
+			client->output_last = add;
+		}
+	}
+	if (client->data_input->stream_errno != 0) {
+		i_error("lmtp client: read(%s) failed: %s",
+			i_stream_get_name(client->data_input),
+			i_stream_get_error(client->data_input));
+		lmtp_client_fail(client,
+			"451 4.3.0 Internal failure while reading DATA input");
+		return -1;
+	}
+	if (sent_bytes && client->data_output_callback != NULL)
+		client->data_output_callback(client->data_output_context);
+
+	if (ret == 0 || ret == -2) {
+		/* -2 can happen with tee istreams */
+		return 0;
+	}
+
+	if (client->output_last != '\n') {
+		/* didn't end with CRLF */
+		o_stream_nsend(client->output, "\r\n", 2);
+	}
+	o_stream_nsend(client->output, ".\r\n", 3);
+	client->output_finished = TRUE;
+	io_loop_time_refresh();
+	client->times.data_sent = ioloop_timeval;
+	return 0;
+}
+
+static void lmtp_client_send_handshake(struct lmtp_client *client)
+{
+	switch (client->protocol) {
+	case LMTP_CLIENT_PROTOCOL_LMTP:
+		o_stream_nsend_str(client->output,
+			t_strdup_printf("LHLO %s\r\n",
+					client->set.my_hostname));
+		break;
+	case LMTP_CLIENT_PROTOCOL_SMTP:
+		o_stream_nsend_str(client->output,
+			t_strdup_printf("EHLO %s\r\n",
+					client->set.my_hostname));
+		break;
+	}
+}
+
+static int lmtp_input_get_reply_code(const char *line, int *reply_code_r,
+				     string_t *multiline)
+{
+	if (!i_isdigit(line[0]) || !i_isdigit(line[1]) || !i_isdigit(line[2]))
+		return -1;
+
+	*reply_code_r = (line[0]-'0') * 100 +
+		(line[1]-'0') * 10 +
+		(line[2]-'0');
+
+	if (line[3] == ' ') {
+		/* final reply */
+		return 1;
+	} else if (line[3] == '-') {
+		/* multiline reply. */
+		str_append(multiline, line);
+		str_append_c(multiline, '\n');
+		return 0;
+	} else {
+		/* invalid input */
+		return -1;
+	}
+}
+
+static void
+lmtp_client_parse_capabilities(struct lmtp_client *client, const char *lines)
+{
+	const char *const *linep;
+
+	for (linep = t_strsplit(lines, "\n"); *linep != NULL; linep++) {
+		const char *line = *linep;
+
+		line += 4; /* already checked this is valid */
+		if (strncasecmp(line, "XCLIENT ", 8) == 0) {
+			client->xclient_args =
+				(void *)p_strsplit(client->pool, line + 8, " ");
+		}
+	}
+}
+
+static bool lmtp_client_send_xclient(struct lmtp_client *client)
+{
+	string_t *str;
+	size_t empty_len;
+
+	if (client->xclient_args == NULL) {
+		/* not supported */
+		return FALSE;
+	}
+	if (client->xclient_sent)
+		return FALSE;
+
+	str = t_str_new(64);
+	str_append(str, "XCLIENT");
+	empty_len = str_len(str);
+	if (client->set.source_ip.family != 0 &&
+	    str_array_icase_find(client->xclient_args, "ADDR"))
+		str_printfa(str, " ADDR=%s", net_ip2addr(&client->set.source_ip));
+	if (client->set.source_port != 0 &&
+	    str_array_icase_find(client->xclient_args, "PORT"))
+		str_printfa(str, " PORT=%u", client->set.source_port);
+	if (client->set.proxy_ttl != 0 &&
+	    str_array_icase_find(client->xclient_args, "TTL"))
+		str_printfa(str, " TTL=%u", client->set.proxy_ttl);
+	if (client->set.proxy_timeout_secs != 0 &&
+	    str_array_icase_find(client->xclient_args, "TIMEOUT"))
+		str_printfa(str, " TIMEOUT=%u", client->set.proxy_timeout_secs);
+
+	if (str_len(str) == empty_len)
+		return FALSE;
+
+	str_append(str, "\r\n");
+	o_stream_nsend(client->output, str_data(str), str_len(str));
+	return TRUE;
+}
+
+static int lmtp_client_input_line(struct lmtp_client *client, const char *line)
+{
+	int ret, reply_code = 0;
+
+	if ((ret = lmtp_input_get_reply_code(line, &reply_code,
+					     client->input_multiline)) <= 0) {
+		if (ret == 0)
+			return 0;
+		lmtp_client_fail(client, t_strdup_printf(
+			"451 4.5.0 Received invalid input: %s", line));
+		return -1;
+	}
+
+	switch (client->input_state) {
+	case LMTP_INPUT_STATE_GREET:
+		if (reply_code != 220) {
+			lmtp_client_fail(client, t_strdup_printf(
+				"451 4.5.0 Received invalid greeting: %s", line));
+			return -1;
+		}
+		client->times.banner_received = ioloop_timeval;
+		lmtp_client_send_handshake(client);
+		client->input_state = LMTP_INPUT_STATE_LHLO;
+		break;
+	case LMTP_INPUT_STATE_XCLIENT:
+		if (reply_code != 220) {
+			lmtp_client_fail(client, t_strdup_printf(
+				"451 4.5.0 XCLIENT failed: %s", line));
+			return -1;
+		}
+		lmtp_client_send_handshake(client);
+		client->input_state = LMTP_INPUT_STATE_LHLO;
+		break;
+	case LMTP_INPUT_STATE_LHLO:
+		if (reply_code != 250) {
+			lmtp_client_fail(client, t_strdup_printf(
+				"451 4.5.0 LHLO failed: %s", line));
+			lmtp_client_fail(client, line);
+			return -1;
+		}
+		str_append(client->input_multiline, line);
+		lmtp_client_parse_capabilities(client,
+			str_c(client->input_multiline));
+		if (lmtp_client_send_xclient(client)) {
+			client->input_state = LMTP_INPUT_STATE_XCLIENT;
+			client->xclient_sent = TRUE;
+			break;
+		}
+		o_stream_nsend_str(client->output, t_strdup_printf(
+			"MAIL FROM:%s\r\n", client->set.mail_from));
+		client->input_state++;
+		break;
+	case LMTP_INPUT_STATE_MAIL_FROM:
+		if (reply_code != 250) {
+			lmtp_client_fail(client, t_strdup_printf(
+				"451 4.5.0 MAIL FROM failed: %s", line));
+			return -1;
+		}
+		client->input_state++;
+		lmtp_client_send_rcpts(client);
+		break;
+	case LMTP_INPUT_STATE_RCPT_TO:
+		lmtp_client_rcpt_next(client, line);
+		if (client->data_input == NULL)
+			break;
+		if (lmtp_client_send_data_cmd(client) < 0)
+			return -1;
+		break;
+	case LMTP_INPUT_STATE_DATA_CONTINUE:
+		/* Start sending DATA */
+		if (strncmp(line, "354", 3) != 0) {
+			lmtp_client_fail_remote(client, line);
+			return -1;
+		}
+		client->input_state++;
+		client->times.data_started = ioloop_timeval;
+		if (client->data_header != NULL)
+			o_stream_nsend_str(client->output, client->data_header);
+		if (lmtp_client_send_data(client) < 0)
+			return -1;
+		break;
+	case LMTP_INPUT_STATE_DATA:
+		/* DATA replies */
+		if (lmtp_client_data_next(client, line) < 0)
+			return -1;
+		break;
+	}
+	return 1;
+}
+
+static void lmtp_client_input(struct lmtp_client *client)
+{
+	const char *line;
+	int ret;
+
+	lmtp_client_ref(client);
+	o_stream_cork(client->output);
+	while ((line = i_stream_read_next_line(client->input)) != NULL) {
+		T_BEGIN {
+			ret = lmtp_client_input_line(client, line);
+		} T_END;
+		if (ret < 0) {
+			o_stream_uncork(client->output);
+			lmtp_client_unref(&client);
+			return;
+		}
+		if (ret > 0)
+			str_truncate(client->input_multiline, 0);
+	}
+
+	if (client->input->stream_errno == ENOBUFS) {
+		lmtp_client_fail(client,
+				 "501 5.5.4 Command reply line too long");
+	} else if (client->input->stream_errno != 0) {
+		i_error("lmtp client: read() failed: %s",
+			i_stream_get_error(client->input));
+		lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
+				 " (read failure)");
+	} else if (client->input->eof) {
+		lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
+				 " (disconnected in input)");
+	}
+	o_stream_uncork(client->output);
+	if (client->to != NULL)
+		timeout_reset(client->to);
+	lmtp_client_unref(&client);
+}
+
+static void lmtp_client_wait_connect(struct lmtp_client *client)
+{
+	int err;
+
+	err = net_geterror(client->fd);
+	if (err != 0) {
+		i_error("lmtp client: connect(%s, %u) failed: %s",
+			client->host, client->port, strerror(err));
+		lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
+				 " (connect)");
+		return;
+	}
+	if (client->to != NULL)
+		timeout_remove(&client->to);
+	io_remove(&client->io);
+	client->io = io_add(client->fd, IO_READ, lmtp_client_input, client);
+	lmtp_client_input(client);
+}
+
+static void lmtp_client_connect_timeout(struct lmtp_client *client)
+{
+	i_error("lmtp client: connect(%s, %u) failed: Timed out in %u secs",
+		client->host, client->port, client->set.timeout_secs);
+	lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
+			 " (connect timeout)");
+}
+
+static int lmtp_client_output(struct lmtp_client *client)
+{
+	int ret;
+
+	lmtp_client_ref(client);
+	o_stream_cork(client->output);
+	if ((ret = o_stream_flush(client->output)) < 0)
+		lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
+				 " (disconnected in output)");
+	else if (client->input_state == LMTP_INPUT_STATE_DATA)
+		(void)lmtp_client_send_data(client);
+	o_stream_uncork(client->output);
+	if (client->to != NULL)
+		timeout_reset(client->to);
+	lmtp_client_unref(&client);
+	return ret;
+}
+
+static int lmtp_client_connect(struct lmtp_client *client)
+{
+	i_assert(client->fd == -1);
+
+	client->times.connect_started = ioloop_timeval;
+
+	client->fd = net_connect_ip(&client->ip, client->port, NULL);
+	if (client->fd == -1) {
+		i_error("lmtp client: connect(%s, %u) failed: %m",
+			client->host, client->port);
+		return -1;
+	}
+	client->input =
+		i_stream_create_fd(client->fd, LMTP_MAX_LINE_LEN, FALSE);
+	client->output = o_stream_create_fd(client->fd, (size_t)-1, FALSE);
+	o_stream_set_no_error_handling(client->output, TRUE);
+	o_stream_set_flush_callback(client->output, lmtp_client_output, client);
+	/* we're already sending data in ostream, so can't use IO_WRITE here */
+	client->io = io_add(client->fd, IO_READ,
+			    lmtp_client_wait_connect, client);
+	if (client->set.timeout_secs > 0) {
+		client->to = timeout_add(client->set.timeout_secs*1000,
+					 lmtp_client_connect_timeout, client);
+	}
+	return 0;
+}
+
+static void lmtp_client_dns_done(const struct dns_lookup_result *result,
+				 struct lmtp_client *client)
+{
+	client->dns_lookup = NULL;
+
+	if (result->ret != 0) {
+		i_error("lmtp client: DNS lookup of %s failed: %s",
+			client->host, result->error);
+		if (client->running) {
+			lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
+					 " (DNS lookup)");
+		}
+	} else {
+		client->ip = result->ips[0];
+		if (lmtp_client_connect(client) < 0 && client->running) {
+			lmtp_client_fail(client, ERRSTR_TEMP_REMOTE_FAILURE
+					 " (connect)");
+		}
+	}
+}
+
+int lmtp_client_connect_tcp(struct lmtp_client *client,
+			    enum lmtp_client_protocol protocol,
+			    const char *host, in_port_t port)
+{
+	struct dns_lookup_settings dns_lookup_set;
+	struct ip_addr *ips;
+	unsigned int ips_count;
+	int ret;
+
+	client->input_state = LMTP_INPUT_STATE_GREET;
+	client->host = p_strdup(client->pool, host);
+	client->port = port;
+	client->protocol = protocol;
+
+	if (*host == '\0') {
+		i_error("lmtp client: host not given");
+		return -1;
+	}
+
+	memset(&dns_lookup_set, 0, sizeof(dns_lookup_set));
+	dns_lookup_set.dns_client_socket_path =
+		client->set.dns_client_socket_path;
+	dns_lookup_set.timeout_msecs = LMTP_CLIENT_DNS_LOOKUP_TIMEOUT_MSECS;
+
+	if (net_addr2ip(host, &client->ip) == 0) {
+		/* IP address */
+	} else if (dns_lookup_set.dns_client_socket_path == NULL) {
+		/* no dns-client, use blocking lookup */
+		ret = net_gethostbyname(host, &ips, &ips_count);
+		if (ret != 0) {
+			i_error("lmtp client: DNS lookup of %s failed: %s",
+				client->host, net_gethosterror(ret));
+			return -1;
+		}
+		client->ip = ips[0];
+	} else {
+		if (dns_lookup(host, &dns_lookup_set,
+			       lmtp_client_dns_done, client,
+			       &client->dns_lookup) < 0)
+			return -1;
+		client->running = TRUE;
+		return 0;
+	}
+
+	if (lmtp_client_connect(client) < 0)
+		return -1;
+	return 0;
+}
+
+void lmtp_client_set_data_header(struct lmtp_client *client, const char *str)
+{
+	client->data_header = p_strdup(client->pool, str);
+}
+
+static void lmtp_append_xtext(string_t *dest, const char *str)
+{
+	unsigned int i;
+
+	for (i = 0; str[i] != '\0'; i++) {
+		if (str[i] >= 33 && str[i] <= 126 &&
+		    str[i] != '+' && str[i] != '=')
+			str_append_c(dest, str[i]);
+		else
+			str_printfa(dest, "+%02X", str[i]);
+	}
+}
+
+static void lmtp_client_send_rcpts(struct lmtp_client *client)
+{
+	const struct lmtp_rcpt *rcpt;
+	unsigned int i, count;
+	string_t *str = t_str_new(128);
+
+	rcpt = array_get(&client->recipients, &count);
+	for (i = client->rcpt_next_send_idx; i < count; i++) {
+		str_truncate(str, 0);
+		str_printfa(str, "RCPT TO:<%s>", rcpt[i].address);
+		if (rcpt->params.dsn_orcpt != NULL) {
+			str_append(str, " ORCPT=");
+			lmtp_append_xtext(str, rcpt->params.dsn_orcpt);
+		}
+		str_append(str, "\r\n");
+		o_stream_nsend(client->output, str_data(str), str_len(str));
+	}
+	client->rcpt_next_send_idx = i;
+}
+
+void lmtp_client_add_rcpt(struct lmtp_client *client, const char *address,
+			  lmtp_callback_t *rcpt_to_callback,
+			  lmtp_callback_t *data_callback, void *context)
+{
+	struct lmtp_recipient_params params;
+
+	memset(&params, 0, sizeof(params));
+	lmtp_client_add_rcpt_params(client, address, &params, rcpt_to_callback,
+				    data_callback, context);
+}
+
+void lmtp_client_add_rcpt_params(struct lmtp_client *client, const char *address,
+				 const struct lmtp_recipient_params *params,
+				 lmtp_callback_t *rcpt_to_callback,
+				 lmtp_callback_t *data_callback, void *context)
+{
+	struct lmtp_rcpt *rcpt;
+	enum lmtp_client_result result;
+
+	rcpt = array_append_space(&client->recipients);
+	rcpt->address = p_strdup(client->pool, address);
+	rcpt->params.dsn_orcpt = p_strdup(client->pool, params->dsn_orcpt);
+	rcpt->rcpt_to_callback = rcpt_to_callback;
+	rcpt->data_callback = data_callback;
+	rcpt->context = context;
+
+	if (client->global_fail_string != NULL) {
+		/* we've already failed */
+		client->rcpt_next_receive_idx++;
+		i_assert(client->rcpt_next_receive_idx ==
+			 array_count(&client->recipients));
+
+		result = client->global_remote_failure ?
+			LMTP_CLIENT_RESULT_REMOTE_ERROR :
+			LMTP_CLIENT_RESULT_INTERNAL_ERROR;
+		rcpt->failed = TRUE;
+		rcpt_to_callback(result, client->global_fail_string, context);
+	} else if (client->input_state == LMTP_INPUT_STATE_RCPT_TO)
+		lmtp_client_send_rcpts(client);
+}
+
+static void lmtp_client_timeout(struct lmtp_client *client)
+{
+	const char *line;
+
+	line = t_strdup_printf(ERRSTR_TEMP_REMOTE_FAILURE
+		" (Timed out after %u secs while waiting for reply to %s)",
+		client->set.timeout_secs, lmtp_client_state_to_string(client));
+	lmtp_client_fail(client, line);
+}
+
+void lmtp_client_send(struct lmtp_client *client, struct istream *data_input)
+{
+	i_assert(client->data_input == NULL);
+
+	i_stream_ref(data_input);
+	client->data_input = data_input;
+
+	/* now we actually want to start doing I/O. start the timeout
+	   handling. */
+	if (client->set.timeout_secs > 0) {
+		if (client->to != NULL) {
+			/* still waiting for connect to finish */
+			timeout_remove(&client->to);
+		}
+		client->to = timeout_add(client->set.timeout_secs*1000,
+					 lmtp_client_timeout, client);
+	}
+
+	(void)lmtp_client_send_data_cmd(client);
+}
+
+void lmtp_client_send_more(struct lmtp_client *client)
+{
+	if (client->input_state == LMTP_INPUT_STATE_DATA) {
+		o_stream_cork(client->output);
+		(void)lmtp_client_send_data(client);
+		o_stream_uncork(client->output);
+	}
+}
+
+void lmtp_client_set_data_output_callback(struct lmtp_client *client,
+					  void (*callback)(void *),
+					  void *context)
+{
+	client->data_output_callback = callback;
+	client->data_output_context = context;
+}
+
+const struct lmtp_client_times *
+lmtp_client_get_times(struct lmtp_client *client)
+{
+	return &client->times;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-smtp/lmtp-client.h	Fri Dec 30 16:20:12 2016 +0100
@@ -0,0 +1,107 @@
+#ifndef LMTP_CLIENT_H
+#define LMTP_CLIENT_H
+
+#include "net.h"
+
+#define ERRSTR_TEMP_REMOTE_FAILURE "451 4.4.0 Remote server not answering"
+
+/* LMTP/SMTP client code. */
+
+enum lmtp_client_protocol {
+	LMTP_CLIENT_PROTOCOL_LMTP,
+	LMTP_CLIENT_PROTOCOL_SMTP
+};
+
+enum lmtp_client_result {
+	/* Command succeeded */
+	LMTP_CLIENT_RESULT_OK = 1,
+	/* Command failed because remote server returned an error */
+	LMTP_CLIENT_RESULT_REMOTE_ERROR = 0,
+	/* Command failed because of an internal error (e.g. couldn't connect
+	   to remote) */
+	LMTP_CLIENT_RESULT_INTERNAL_ERROR = -1
+};
+
+struct lmtp_recipient_params {
+	const char *dsn_orcpt;
+};
+
+struct lmtp_client_times {
+	struct timeval connect_started;
+	struct timeval banner_received;
+	struct timeval data_started;
+	struct timeval data_sent;
+};
+
+struct lmtp_client_settings {
+	const char *my_hostname;
+	/* The whole MAIL FROM line, including parameters */
+	const char *mail_from;
+	const char *dns_client_socket_path;
+
+	/* if remote server supports XCLIENT capability,
+	   send the these as ADDR/PORT/TTL/TIMEOUT */
+	struct ip_addr source_ip;
+	in_port_t source_port;
+	/* send TTL as this (default 0 means "don't send it") */
+	unsigned int proxy_ttl;
+	/* remote is notified that the connection is going to be closed after
+	   this many seconds, so it should try to keep lock waits and such
+	   lower than this. */
+	unsigned int proxy_timeout_secs;
+	/* Don't wait an answer from destination server longer than this many
+	   seconds (0 = unlimited) */
+	unsigned int timeout_secs;
+};
+
+/* reply contains the reply coming from remote server, or NULL
+   if it's a connection error. */
+typedef void lmtp_callback_t(enum lmtp_client_result result,
+			     const char *reply, void *context);
+/* called when session is finished, either because all RCPT TOs failed or
+   because all DATA replies have been received. */
+typedef void lmtp_finish_callback_t(void *context);
+
+struct lmtp_client *
+lmtp_client_init(const struct lmtp_client_settings *set,
+		 lmtp_finish_callback_t *finish_callback, void *context);
+void lmtp_client_deinit(struct lmtp_client **client);
+
+int lmtp_client_connect_tcp(struct lmtp_client *client,
+			    enum lmtp_client_protocol protocol,
+			    const char *host, in_port_t port);
+void lmtp_client_close(struct lmtp_client *client);
+
+/* Add headers from given string before the rest of the data. The string must
+   use CRLF line feeds and end with CRLF. */
+void lmtp_client_set_data_header(struct lmtp_client *client, const char *str);
+/* Add recipient to the session. rcpt_to_callback is called once LMTP server
+   replies with RCPT TO. If RCPT TO was a succees, data_callback is called
+   when DATA replies. */
+void lmtp_client_add_rcpt(struct lmtp_client *client, const char *address,
+			  lmtp_callback_t *rcpt_to_callback,
+			  lmtp_callback_t *data_callback, void *context);
+void lmtp_client_add_rcpt_params(struct lmtp_client *client, const char *address,
+				 const struct lmtp_recipient_params *params,
+				 lmtp_callback_t *rcpt_to_callback,
+				 lmtp_callback_t *data_callback, void *context);
+/* Start sending input stream as DATA. */
+void lmtp_client_send(struct lmtp_client *client, struct istream *data_input);
+/* Call this function whenever input stream can potentially be read forward.
+   This is useful with non-blocking istreams and tee-istreams. */
+void lmtp_client_send_more(struct lmtp_client *client);
+/* Fail the connection with line as the reply to unfinished RCPT TO/DATA
+   replies. This will be treated as an internal failure. */
+void lmtp_client_fail(struct lmtp_client *client, const char *line);
+/* Return the state (command reply) the client is currently waiting for. */
+const char *lmtp_client_state_to_string(struct lmtp_client *client);
+/* Call the given callback whenever client manages to send some more DATA
+   output to client. */
+void lmtp_client_set_data_output_callback(struct lmtp_client *client,
+					  void (*callback)(void *),
+					  void *context);
+/* Return LMTP client statistics. */
+const struct lmtp_client_times *
+lmtp_client_get_times(struct lmtp_client *client);
+
+#endif
--- a/src/lmtp/Makefile.am	Fri Oct 28 21:34:41 2016 +0300
+++ b/src/lmtp/Makefile.am	Fri Dec 30 16:20:12 2016 +0100
@@ -7,6 +7,7 @@
 	-I$(top_srcdir)/src/lib-settings \
 	-I$(top_srcdir)/src/lib-auth \
 	-I$(top_srcdir)/src/lib-mail \
+	-I$(top_srcdir)/src/lib-smtp \
 	-I$(top_srcdir)/src/lib-imap \
 	-I$(top_srcdir)/src/lib-index \
 	-I$(top_srcdir)/src/lib-master \