]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-lda: Created tests for SMTP message submission using the smtp-client API.
authorStephan Bosch <stephan.bosch@dovecot.fi>
Tue, 28 Feb 2017 22:56:47 +0000 (23:56 +0100)
committerTimo Sirainen <tss@dovecot.fi>
Fri, 8 Sep 2017 15:16:36 +0000 (18:16 +0300)
src/lib-lda/Makefile.am
src/lib-lda/test-bin/sendmail-exit-1.sh [new file with mode: 0755]
src/lib-lda/test-bin/sendmail-success.sh [new file with mode: 0755]
src/lib-lda/test-smtp-client.c [new file with mode: 0644]

index b02828508d93f10db3c16380a9fdfdc2c5d8a76c..54cad69036e3b6f2fc15dbe8f3379dbbaa4d9c10 100644 (file)
@@ -2,6 +2,7 @@ noinst_LTLIBRARIES = liblda.la
 
 AM_CPPFLAGS = \
        -I$(top_srcdir)/src/lib \
+       -I$(top_srcdir)/src/lib-test \
        -I$(top_srcdir)/src/lib-settings \
        -I$(top_srcdir)/src/lib-master \
        -I$(top_srcdir)/src/lib-dns \
@@ -10,7 +11,8 @@ AM_CPPFLAGS = \
        -I$(top_srcdir)/src/lib-imap \
        -I$(top_srcdir)/src/lib-mail \
        -I$(top_srcdir)/src/lib-storage \
-       -I$(top_srcdir)/src/lib-program-client
+       -I$(top_srcdir)/src/lib-program-client \
+       -DTEST_BIN_DIR=\"$(abs_srcdir)/test-bin\"
 
 liblda_la_SOURCES = \
        duplicate.c \
@@ -36,3 +38,46 @@ libdovecot_lda_la_SOURCES =
 libdovecot_lda_la_LIBADD = liblda.la $(deps)
 libdovecot_lda_la_DEPENDENCIES = liblda.la $(deps)
 libdovecot_lda_la_LDFLAGS = -export-dynamic
+
+test_programs =
+
+test_nocheck_programs = \
+       test-smtp-client
+
+noinst_PROGRAMS = $(test_programs) $(test_nocheck_programs)
+
+test_libs = \
+       liblda.la \
+       ../lib-smtp/libsmtp.la \
+       ../lib-program-client/libprogram_client.la \
+       ../lib-dns/libdns.la \
+       ../lib-mail/libmail.la \
+       ../lib-master/libmaster.la \
+       ../lib-ssl-iostream/libssl_iostream.la \
+       ../lib-settings/libsettings.la \
+       ../lib-test/libtest.la \
+       ../lib/liblib.la \
+       $(MODULE_LIBS)
+
+test_deps = \
+       liblda.la \
+       ../lib-smtp/libsmtp.la \
+       ../lib-program-client/libprogram_client.la \
+       ../lib-dns/libdns.la \
+       ../lib-mail/libmail.la \
+       ../lib-master/libmaster.la \
+       ../lib-ssl-iostream/libssl_iostream.la \
+       ../lib-settings/libsettings.la \
+       ../lib-test/libtest.la \
+       ../lib/liblib.la
+
+test_smtp_client_SOURCES = test-smtp-client.c
+test_smtp_client_LDFLAGS = -export-dynamic
+test_smtp_client_LDADD = $(test_libs)
+test_smtp_client_DEPENDENCIES = $(test_deps)
+
+check: check-am check-test
+check-test: all-am
+       for bin in $(test_programs); do \
+         if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+       done
diff --git a/src/lib-lda/test-bin/sendmail-exit-1.sh b/src/lib-lda/test-bin/sendmail-exit-1.sh
new file mode 100755 (executable)
index 0000000..195df7c
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exit 1;
diff --git a/src/lib-lda/test-bin/sendmail-success.sh b/src/lib-lda/test-bin/sendmail-success.sh
new file mode 100755 (executable)
index 0000000..bb09b2b
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+cat > $1
+exit 0;
diff --git a/src/lib-lda/test-smtp-client.c b/src/lib-lda/test-smtp-client.c
new file mode 100644 (file)
index 0000000..bb8f176
--- /dev/null
@@ -0,0 +1,1961 @@
+/* Copyright (c) 2016-2017 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "str.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-chain.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "unlink-directory.h"
+#include "write-full.h"
+#include "connection.h"
+#include "master-service.h"
+#include "istream-dot.h"
+#include "test-common.h"
+#include "lda-settings.h"
+#include "smtp-client.h"
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/stat.h>
+#include <signal.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+static const char *test_message1 =
+       "Subject: Test message\r\n"
+       "To: rcpt@example.com\r\n"
+       "From: sender@example.com\r\n"
+       "\r\n"
+       "Test message\r\n";
+
+/*
+ * Types
+ */
+
+struct server_connection {
+       struct connection conn;
+
+       void *context;
+
+       pool_t pool;
+};
+
+typedef void (*test_server_init_t)(unsigned int index);
+typedef bool (*test_client_init_t)
+       (const struct lda_settings *client_set);
+
+/*
+ * State
+ */
+
+/* common */
+static struct ip_addr bind_ip;
+static in_port_t *bind_ports = NULL;
+static struct ioloop *ioloop;
+static bool debug = FALSE;
+static char *tmp_dir = NULL;
+
+/* server */
+static struct io *io_listen;
+static int fd_listen = -1;
+static pid_t *server_pids = NULL;
+static in_port_t server_port = 0;
+static unsigned int server_pids_count = 0;
+static struct connection_list *server_conn_list;
+static unsigned int server_index;
+static void (*test_server_input)(struct server_connection *conn);
+static void (*test_server_init)(struct server_connection *conn);
+static void (*test_server_deinit)(struct server_connection *conn);
+
+/* client */
+
+/*
+ * Forward declarations
+ */
+
+/* server */
+static void test_server_run(unsigned int index);
+static void
+server_connection_deinit(struct server_connection **_conn);
+
+/* client */
+static void
+test_client_defaults(struct lda_settings *smtp_set);
+static void test_client_deinit(void);
+
+static int
+test_client_smtp_send_simple(const struct lda_settings *smtp_set,
+       const char *message, const char *host,
+       unsigned int timeout_secs, const char **error_r);
+static int
+test_client_smtp_send_simple_port(const struct lda_settings *smtp_set,
+       const char *message, unsigned int port,
+       unsigned int timeout_secs, const char **error_r);
+
+/* test*/
+static const char *test_tmp_dir_get(void);
+
+static void
+test_message_delivery(const char *message, const char *file);
+
+static void test_run_client_server(
+       const struct lda_settings *client_set,
+       test_client_init_t client_test,
+       test_server_init_t server_test,
+       unsigned int server_tests_count)
+       ATTR_NULL(3);
+
+/*
+ * Host lookup failed
+ */
+
+/* client */
+
+static bool
+test_client_host_lookup_failed(const struct lda_settings *client_set)
+{
+       const char *error = NULL;
+       int ret;
+
+       ret = test_client_smtp_send_simple(client_set,
+               test_message1, "host.invalid", 5, &error);
+       test_out_reason("run (ret < 0)", ret < 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_host_lookup_failed(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("host lookup failed");
+       test_expect_errors(1);
+       test_run_client_server(&smtp_client_set,
+               test_client_host_lookup_failed,
+               NULL, 0);
+       test_end();
+}
+
+/*
+ * Connection refused
+ */
+
+/* server */
+
+static void
+test_server_connection_refused(unsigned int index ATTR_UNUSED)
+{
+       i_close_fd(&fd_listen);
+}
+
+/* client */
+
+static bool
+test_client_connection_refused(const struct lda_settings *client_set)
+{
+       const char *error = NULL;
+       int ret;
+
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 5, &error);
+       test_out_reason("run (ret < 0)", ret < 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_connection_refused(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("connection refused");
+       test_expect_errors(1);
+       test_run_client_server(&smtp_client_set,
+               test_client_connection_refused,
+               test_server_connection_refused, 1);
+       test_end();
+}
+
+/*
+ * Connection timed out
+ */
+
+/* server */
+
+static void
+test_connection_timed_out_input(struct server_connection *conn)
+{
+       sleep(10);
+       server_connection_deinit(&conn);
+}
+
+static void test_server_connection_timed_out(unsigned int index)
+{
+       test_server_input = test_connection_timed_out_input;
+       test_server_run(index);
+}
+/* client */
+
+static bool
+test_client_connection_timed_out(const struct lda_settings *client_set)
+{
+       time_t time;
+       const char *error = NULL;
+       int ret;
+
+       io_loop_time_refresh();
+       time = ioloop_time;
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 1, &error);
+       test_out_reason("run (ret < 0)", ret < 0, error);
+
+       io_loop_time_refresh();
+       test_out("timeout", (ioloop_time - time) < 5);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_connection_timed_out(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("connection timed out");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_connection_timed_out,
+               test_server_connection_timed_out, 1);
+       test_end();
+}
+
+/*
+ * Bad greeting
+ */
+
+/* server */
+
+static void
+test_bad_greeting_input(struct server_connection *conn)
+{
+       server_connection_deinit(&conn);
+}
+
+static void
+test_bad_greeting_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "554 No SMTP service here.\r\n");
+}
+
+static void test_server_bad_greeting(unsigned int index)
+{
+       test_server_init = test_bad_greeting_init;
+       test_server_input = test_bad_greeting_input;
+       test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_bad_greeting(const struct lda_settings *client_set)
+{
+       const char *error = NULL;
+       int ret;
+
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 5, &error);
+       // FIXME: lmtp client handles this wrong, the greeting is not "bad"
+       //test_out_reason("run", ret == 0, error);
+       test_out_reason("run (ret < 0)", ret < 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_bad_greeting(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("bad greeting");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_bad_greeting,
+               test_server_bad_greeting, 1);
+       test_end();
+}
+
+/*
+ * Denied HELO
+ */
+
+/* server */
+
+static void
+test_denied_helo_input(struct server_connection *conn)
+{
+       const char *line;
+
+       line = i_stream_read_next_line(conn->conn.input);
+       if (line == NULL) {
+               if (i_stream_is_eof(conn->conn.input) ||
+                       conn->conn.input->stream_errno != 0)
+                       server_connection_deinit(&conn);
+               return;
+       }
+       o_stream_send_str(conn->conn.output,
+               "502 Command not implemented\r\n");
+       server_connection_deinit(&conn);
+}
+
+static void
+test_denied_helo_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_denied_helo(unsigned int index)
+{
+       test_server_init = test_denied_helo_init;
+       test_server_input = test_denied_helo_input;
+       test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_denied_helo(const struct lda_settings *client_set)
+{
+       const char *error = NULL;
+       int ret;
+
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 5, &error);
+       // FIXME: lmtp client handles this wrong, the greeting is not "bad"
+       //test_out_reason("run", ret == 0, error);
+       test_out_reason("run (ret < 0)", ret < 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_denied_helo(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("denied helo");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_denied_helo,
+               test_server_denied_helo, 1);
+       test_end();
+}
+
+/*
+ * Disconnect HELO
+ */
+
+/* server */
+
+static void
+test_disconnect_helo_input(struct server_connection *conn)
+{
+       const char *line;
+
+       line = i_stream_read_next_line(conn->conn.input);
+       if (line == NULL) {
+               if (i_stream_is_eof(conn->conn.input) ||
+                       conn->conn.input->stream_errno != 0)
+                       server_connection_deinit(&conn);
+               return;
+       }
+       server_connection_deinit(&conn);
+}
+
+static void
+test_disconnect_helo_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_disconnect_helo(unsigned int index)
+{
+       test_server_init = test_disconnect_helo_init;
+       test_server_input = test_disconnect_helo_input;
+       test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_disconnect_helo(const struct lda_settings *client_set)
+{
+       const char *error = NULL;
+       int ret;
+
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 5, &error);
+       // FIXME: lmtp client handles this wrong, the greeting is not "bad"
+       //test_out_reason("run", ret == 0, error);
+       test_out_reason("run (ret < 0)", ret < 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_disconnect_helo(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("disconnect helo");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_disconnect_helo,
+               test_server_disconnect_helo, 1);
+       test_end();
+}
+
+/*
+ * Denied MAIL
+ */
+
+/* server */
+
+enum _denied_mail_state {
+       DENIED_MAIL_STATE_EHLO = 0,
+       DENIED_MAIL_STATE_MAIL_FROM
+};
+
+struct _denied_mail_server {
+       enum _denied_mail_state state;
+};
+
+static void
+test_denied_mail_input(struct server_connection *conn)
+{
+       struct _denied_mail_server *ctx;
+       const char *line;
+
+       if (conn->context == NULL) {
+               ctx = p_new(conn->pool, struct _denied_mail_server, 1);
+               conn->context = (void*)ctx;
+       } else {
+               ctx = (struct _denied_mail_server *)conn->context;
+       }
+
+       for (;;) {
+               line = i_stream_read_next_line(conn->conn.input);
+               if (line == NULL) {
+                       if (i_stream_is_eof(conn->conn.input) ||
+                               conn->conn.input->stream_errno != 0)
+                               server_connection_deinit(&conn);
+                       return;
+               }
+
+               switch (ctx->state) {
+               case DENIED_MAIL_STATE_EHLO:
+                       o_stream_send_str(conn->conn.output,
+                               "250-testserver\r\n"
+                               "250-PIPELINING\r\n"
+                               "250-ENHANCEDSTATUSCODES\r\n"
+                               "250-8BITMIME\r\n"
+                               "250 DSN\r\n");
+                       ctx->state = DENIED_MAIL_STATE_MAIL_FROM;
+                       return;
+               case DENIED_MAIL_STATE_MAIL_FROM:
+                       o_stream_send_str(conn->conn.output,
+                               "453 4.3.2 Incapable of accepting messages at this time.\r\n");
+                       server_connection_deinit(&conn);
+                       return;
+               }
+               i_unreached();
+       }
+}
+
+static void
+test_denied_mail_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_denied_mail(unsigned int index)
+{
+       test_server_init = test_denied_mail_init;
+       test_server_input = test_denied_mail_input;
+       test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_denied_mail(const struct lda_settings *client_set)
+{
+       const char *error = NULL;
+       int ret;
+
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 5, &error);
+       // FIXME: lmtp client handles this wrong, the greeting is not "bad"
+       //test_out_reason("run", ret == 0, error);
+       test_out_reason("run (ret < 0)", ret < 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_denied_mail(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("denied mail");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_denied_mail,
+               test_server_denied_mail, 1);
+       test_end();
+}
+
+/*
+ * Denied RCPT
+ */
+
+/* server */
+
+enum _denied_rcpt_state {
+       DENIED_RCPT_STATE_EHLO = 0,
+       DENIED_RCPT_STATE_MAIL_FROM,
+       DENIED_RCPT_STATE_RCPT_TO
+};
+
+struct _denied_rcpt_server {
+       enum _denied_rcpt_state state;
+};
+
+static void
+test_denied_rcpt_input(struct server_connection *conn)
+{
+       struct _denied_rcpt_server *ctx;
+       const char *line;
+
+       if (conn->context == NULL) {
+               ctx = p_new(conn->pool, struct _denied_rcpt_server, 1);
+               conn->context = (void*)ctx;
+       } else {
+               ctx = (struct _denied_rcpt_server *)conn->context;
+       }
+
+       for (;;) {
+               line = i_stream_read_next_line(conn->conn.input);
+               if (line == NULL) {
+                       if (i_stream_is_eof(conn->conn.input) ||
+                               conn->conn.input->stream_errno != 0)
+                               server_connection_deinit(&conn);
+                       return;
+               }
+
+               switch (ctx->state) {
+               case DENIED_RCPT_STATE_EHLO:
+                       o_stream_send_str(conn->conn.output,
+                               "250-testserver\r\n"
+                               "250-PIPELINING\r\n"
+                               "250-ENHANCEDSTATUSCODES\r\n"
+                               "250-8BITMIME\r\n"
+                               "250 DSN\r\n");
+                       ctx->state = DENIED_RCPT_STATE_MAIL_FROM;
+                       return;
+               case DENIED_RCPT_STATE_MAIL_FROM:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.0 Ok\r\n");
+                       ctx->state = DENIED_RCPT_STATE_RCPT_TO;
+                       continue;
+               case DENIED_RCPT_STATE_RCPT_TO:
+                       o_stream_send_str(conn->conn.output,
+                               "550 5.4.3 Directory server failure\r\n");
+                       server_connection_deinit(&conn);
+                       return;
+               }
+               i_unreached();
+       }
+}
+
+static void
+test_denied_rcpt_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_denied_rcpt(unsigned int index)
+{
+       test_server_init = test_denied_rcpt_init;
+       test_server_input = test_denied_rcpt_input;
+       test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_denied_rcpt(const struct lda_settings *client_set)
+{
+       const char *error = NULL;
+       int ret;
+
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 5, &error);
+       test_out_reason("run (ret == 0)", ret == 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_denied_rcpt(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("denied rcpt");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_denied_rcpt,
+               test_server_denied_rcpt, 1);
+       test_end();
+}
+
+/*
+ * Denied second RCPT
+ */
+
+/* server */
+
+enum _denied_second_rcpt_state {
+       DENIED_SECOND_RCPT_STATE_EHLO = 0,
+       DENIED_SECOND_RCPT_STATE_MAIL_FROM,
+       DENIED_SECOND_RCPT_STATE_RCPT_TO,
+       DENIED_SECOND_RCPT_STATE_RCPT_TO2
+};
+
+struct _denied_second_rcpt_server {
+       enum _denied_second_rcpt_state state;
+};
+
+static void
+test_denied_second_rcpt_input(struct server_connection *conn)
+{
+       struct _denied_second_rcpt_server *ctx;
+       const char *line;
+
+       if (conn->context == NULL) {
+               ctx = p_new(conn->pool, struct _denied_second_rcpt_server, 1);
+               conn->context = (void*)ctx;
+       } else {
+               ctx = (struct _denied_second_rcpt_server *)conn->context;
+       }
+
+       for (;;) {
+               line = i_stream_read_next_line(conn->conn.input);
+               if (line == NULL) {
+                       if (i_stream_is_eof(conn->conn.input) ||
+                               conn->conn.input->stream_errno != 0)
+                               server_connection_deinit(&conn);
+                       return;
+               }
+
+               switch (ctx->state) {
+               case DENIED_SECOND_RCPT_STATE_EHLO:
+                       o_stream_send_str(conn->conn.output,
+                               "250-testserver\r\n"
+                               "250-PIPELINING\r\n"
+                               "250-ENHANCEDSTATUSCODES\r\n"
+                               "250-8BITMIME\r\n"
+                               "250 DSN\r\n");
+                       ctx->state = DENIED_SECOND_RCPT_STATE_MAIL_FROM;
+                       return;
+               case DENIED_SECOND_RCPT_STATE_MAIL_FROM:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.0 Ok\r\n");
+                       ctx->state = DENIED_SECOND_RCPT_STATE_RCPT_TO;
+                       continue;
+               case DENIED_SECOND_RCPT_STATE_RCPT_TO:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.5 Ok\r\n");
+                       ctx->state = DENIED_SECOND_RCPT_STATE_RCPT_TO2;
+                       continue;
+               case DENIED_SECOND_RCPT_STATE_RCPT_TO2:
+                       o_stream_send_str(conn->conn.output,
+                               "550 5.4.3 Directory server failure\r\n");
+                       server_connection_deinit(&conn);
+                       return;
+               }
+               i_unreached();
+       }
+}
+
+static void
+test_denied_second_rcpt_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_denied_second_rcpt(unsigned int index)
+{
+       test_server_init = test_denied_second_rcpt_init;
+       test_server_input = test_denied_second_rcpt_input;
+       test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_denied_second_rcpt(const struct lda_settings *client_set)
+{
+       struct smtp_client *smtp_client;
+       struct lda_settings smtp_client_set;
+       struct ostream *output;
+       const char *error = NULL;
+       int ret;
+
+       smtp_client_set = *client_set;
+       smtp_client_set.submission_host =
+               t_strdup_printf("127.0.0.1:%u", bind_ports[0]);
+
+       smtp_client = smtp_client_init(&smtp_client_set, "sender@example.com");
+
+       smtp_client_add_rcpt(smtp_client, "rcpt@example.com");
+       smtp_client_add_rcpt(smtp_client, "rcpt2@example.com");
+       output = smtp_client_send(smtp_client);
+       o_stream_send_str(output, test_message1);
+
+       ret = smtp_client_deinit_timeout(smtp_client, 5, &error);
+       test_out_reason("run (ret == 0)", ret == 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_denied_second_rcpt(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("denied second rcpt");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_denied_second_rcpt,
+               test_server_denied_second_rcpt, 1);
+       test_end();
+}
+
+/*
+ * Denied DATA
+ */
+
+/* server */
+
+enum _denied_data_state {
+       DENIED_DATA_STATE_EHLO = 0,
+       DENIED_DATA_STATE_MAIL_FROM,
+       DENIED_DATA_STATE_RCPT_TO,
+       DENIED_DATA_STATE_DATA
+};
+
+struct _denied_data_server {
+       enum _denied_data_state state;
+};
+
+static void
+test_denied_data_input(struct server_connection *conn)
+{
+       struct _denied_data_server *ctx;
+       const char *line;
+
+       if (conn->context == NULL) {
+               ctx = p_new(conn->pool, struct _denied_data_server, 1);
+               conn->context = (void*)ctx;
+       } else {
+               ctx = (struct _denied_data_server *)conn->context;
+       }
+
+       for (;;) {
+               line = i_stream_read_next_line(conn->conn.input);
+               if (line == NULL) {
+                       if (i_stream_is_eof(conn->conn.input) ||
+                               conn->conn.input->stream_errno != 0)
+                               server_connection_deinit(&conn);
+                       return;
+               }
+
+               switch (ctx->state) {
+               case DENIED_DATA_STATE_EHLO:
+                       o_stream_send_str(conn->conn.output,
+                               "250-testserver\r\n"
+                               "250-PIPELINING\r\n"
+                               "250-ENHANCEDSTATUSCODES\r\n"
+                               "250-8BITMIME\r\n"
+                               "250 DSN\r\n");
+                       ctx->state = DENIED_DATA_STATE_MAIL_FROM;
+                       return;
+               case DENIED_DATA_STATE_MAIL_FROM:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.0 Ok\r\n");
+                       ctx->state = DENIED_DATA_STATE_RCPT_TO;
+                       continue;
+               case DENIED_DATA_STATE_RCPT_TO:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.5 Ok\r\n");
+                       ctx->state = DENIED_DATA_STATE_DATA;
+                       continue;
+               case DENIED_DATA_STATE_DATA:
+                       o_stream_send_str(conn->conn.output,
+                               "500 5.0.0 Unacceptabe recipients\r\n");
+                       server_connection_deinit(&conn);
+                       return;
+               }
+               i_unreached();
+       }
+}
+
+static void
+test_denied_data_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_denied_data(unsigned int index)
+{
+       test_server_init = test_denied_data_init;
+       test_server_input = test_denied_data_input;
+       test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_denied_data(const struct lda_settings *client_set)
+{
+       const char *error = NULL;
+       int ret;
+
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 5, &error);
+       test_out_reason("run (ret == 0)", ret == 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_denied_data(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("denied data");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_denied_data,
+               test_server_denied_data, 1);
+       test_end();
+}
+
+/*
+ * Data failure
+ */
+
+/* server */
+
+enum _data_failure_state {
+       DATA_FAILURE_STATE_EHLO = 0,
+       DATA_FAILURE_STATE_MAIL_FROM,
+       DATA_FAILURE_STATE_RCPT_TO,
+       DATA_FAILURE_STATE_DATA,
+       DATA_FAILURE_STATE_FINISH
+};
+
+struct _data_failure_server {
+       enum _data_failure_state state;
+};
+
+static void
+test_data_failure_input(struct server_connection *conn)
+{
+       struct _data_failure_server *ctx;
+       const char *line;
+
+       if (conn->context == NULL) {
+               ctx = p_new(conn->pool, struct _data_failure_server, 1);
+               conn->context = (void*)ctx;
+       } else {
+               ctx = (struct _data_failure_server *)conn->context;
+       }
+
+       for (;;) {
+               line = i_stream_read_next_line(conn->conn.input);
+               if (line == NULL) {
+                       if (i_stream_is_eof(conn->conn.input) ||
+                               conn->conn.input->stream_errno != 0)
+                               server_connection_deinit(&conn);
+                       return;
+               }
+
+               switch (ctx->state) {
+               case DATA_FAILURE_STATE_EHLO:
+                       o_stream_send_str(conn->conn.output,
+                               "250-testserver\r\n"
+                               "250-PIPELINING\r\n"
+                               "250-ENHANCEDSTATUSCODES\r\n"
+                               "250-8BITMIME\r\n"
+                               "250 DSN\r\n");
+                       ctx->state = DATA_FAILURE_STATE_MAIL_FROM;
+                       return;
+               case DATA_FAILURE_STATE_MAIL_FROM:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.0 Ok\r\n");
+                       ctx->state = DATA_FAILURE_STATE_RCPT_TO;
+                       continue;
+               case DATA_FAILURE_STATE_RCPT_TO:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.5 Ok\r\n");
+                       ctx->state = DATA_FAILURE_STATE_DATA;
+                       continue;
+               case DATA_FAILURE_STATE_DATA:
+                       o_stream_send_str(conn->conn.output,
+                               "354 End data with <CR><LF>.<CR><LF>\r\n");
+                       ctx->state = DATA_FAILURE_STATE_FINISH;
+                       continue;
+               case DATA_FAILURE_STATE_FINISH:
+                       if (strcmp(line, ".") == 0) {
+                               o_stream_send_str(conn->conn.output,
+                                       "552 5.2.3 Message length exceeds administrative limit\r\n");
+                               server_connection_deinit(&conn);
+                               return;
+                       }
+                       continue;
+               }
+               i_unreached();
+       }
+}
+
+static void
+test_data_failure_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_data_failure(unsigned int index)
+{
+       test_server_init = test_data_failure_init;
+       test_server_input = test_data_failure_input;
+       test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_data_failure(const struct lda_settings *client_set)
+{
+       const char *error = NULL;
+       int ret;
+
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 5, &error);
+       test_out_reason("run (ret == 0)", ret == 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_data_failure(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("data failure");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_data_failure,
+               test_server_data_failure, 1);
+       test_end();
+}
+
+/*
+ * Data disconnect
+ */
+
+/* server */
+
+enum _data_disconnect_state {
+       DATA_DISCONNECT_STATE_EHLO = 0,
+       DATA_DISCONNECT_STATE_MAIL_FROM,
+       DATA_DISCONNECT_STATE_RCPT_TO,
+       DATA_DISCONNECT_STATE_DATA,
+       DATA_DISCONNECT_STATE_FINISH
+};
+
+struct _data_disconnect_server {
+       enum _data_disconnect_state state;
+};
+
+static void
+test_data_disconnect_input(struct server_connection *conn)
+{
+       struct _data_disconnect_server *ctx;
+       const char *line;
+
+       if (conn->context == NULL) {
+               ctx = p_new(conn->pool, struct _data_disconnect_server, 1);
+               conn->context = (void*)ctx;
+       } else {
+               ctx = (struct _data_disconnect_server *)conn->context;
+       }
+
+       for (;;) {
+               line = i_stream_read_next_line(conn->conn.input);
+               if (line == NULL) {
+                       if (i_stream_is_eof(conn->conn.input) ||
+                               conn->conn.input->stream_errno != 0)
+                               server_connection_deinit(&conn);
+                       return;
+               }
+
+               switch (ctx->state) {
+               case DATA_DISCONNECT_STATE_EHLO:
+                       o_stream_send_str(conn->conn.output,
+                               "250-testserver\r\n"
+                               "250-PIPELINING\r\n"
+                               "250-ENHANCEDSTATUSCODES\r\n"
+                               "250-8BITMIME\r\n"
+                               "250 DSN\r\n");
+                       ctx->state = DATA_DISCONNECT_STATE_MAIL_FROM;
+                       return;
+               case DATA_DISCONNECT_STATE_MAIL_FROM:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.0 Ok\r\n");
+                       ctx->state = DATA_DISCONNECT_STATE_RCPT_TO;
+                       continue;
+               case DATA_DISCONNECT_STATE_RCPT_TO:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.5 Ok\r\n");
+                       ctx->state = DATA_DISCONNECT_STATE_DATA;
+                       continue;
+               case DATA_DISCONNECT_STATE_DATA:
+                       o_stream_send_str(conn->conn.output,
+                               "354 End data with <CR><LF>.<CR><LF>\r\n");
+                       ctx->state = DATA_DISCONNECT_STATE_FINISH;
+                       continue;
+               case DATA_DISCONNECT_STATE_FINISH:
+                       server_connection_deinit(&conn);
+                       return;
+               }
+               i_unreached();
+       }
+}
+
+static void
+test_data_disconnect_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_data_disconnect(unsigned int index)
+{
+       test_server_init = test_data_disconnect_init;
+       test_server_input = test_data_disconnect_input;
+       test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_data_disconnect(const struct lda_settings *client_set)
+{
+       const char *error = NULL;
+       int ret;
+
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 5, &error);
+       test_out_reason("run (ret < 0)", ret < 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_data_disconnect(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("data disconnect");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_data_disconnect,
+               test_server_data_disconnect, 1);
+       test_end();
+}
+
+/*
+ * Data timout
+ */
+
+/* server */
+
+enum _data_timout_state {
+       DATA_TIMEOUT_STATE_EHLO = 0,
+       DATA_TIMEOUT_STATE_MAIL_FROM,
+       DATA_TIMEOUT_STATE_RCPT_TO,
+       DATA_TIMEOUT_STATE_DATA,
+       DATA_TIMEOUT_STATE_FINISH
+};
+
+struct _data_timout_server {
+       enum _data_timout_state state;
+};
+
+static void
+test_data_timout_input(struct server_connection *conn)
+{
+       struct _data_timout_server *ctx;
+       const char *line;
+
+       if (conn->context == NULL) {
+               ctx = p_new(conn->pool, struct _data_timout_server, 1);
+               conn->context = (void*)ctx;
+       } else {
+               ctx = (struct _data_timout_server *)conn->context;
+       }
+
+       for (;;) {
+               line = i_stream_read_next_line(conn->conn.input);
+               if (line == NULL) {
+                       if (i_stream_is_eof(conn->conn.input) ||
+                               conn->conn.input->stream_errno != 0)
+                               server_connection_deinit(&conn);
+                       return;
+               }
+
+               switch (ctx->state) {
+               case DATA_TIMEOUT_STATE_EHLO:
+                       o_stream_send_str(conn->conn.output,
+                               "250-testserver\r\n"
+                               "250-PIPELINING\r\n"
+                               "250-ENHANCEDSTATUSCODES\r\n"
+                               "250-8BITMIME\r\n"
+                               "250 DSN\r\n");
+                       ctx->state = DATA_TIMEOUT_STATE_MAIL_FROM;
+                       return;
+               case DATA_TIMEOUT_STATE_MAIL_FROM:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.0 Ok\r\n");
+                       ctx->state = DATA_TIMEOUT_STATE_RCPT_TO;
+                       continue;
+               case DATA_TIMEOUT_STATE_RCPT_TO:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.5 Ok\r\n");
+                       ctx->state = DATA_TIMEOUT_STATE_DATA;
+                       continue;
+               case DATA_TIMEOUT_STATE_DATA:
+                       o_stream_send_str(conn->conn.output,
+                               "354 End data with <CR><LF>.<CR><LF>\r\n");
+                       ctx->state = DATA_TIMEOUT_STATE_FINISH;
+                       continue;
+               case DATA_TIMEOUT_STATE_FINISH:
+                       sleep(10);
+                       server_connection_deinit(&conn);
+                       return;
+               }
+               i_unreached();
+       }
+}
+
+static void
+test_data_timout_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_data_timout(unsigned int index)
+{
+       test_server_init = test_data_timout_init;
+       test_server_input = test_data_timout_input;
+       test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_data_timout(const struct lda_settings *client_set)
+{
+       time_t time;
+       const char *error = NULL;
+       int ret;
+
+       io_loop_time_refresh();
+       time = ioloop_time;
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 2, &error);
+       test_out_reason("run (ret < 0)", ret < 0, error);
+
+       io_loop_time_refresh();
+       test_out("timeout", (ioloop_time - time) < 5);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_data_timeout(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("data timeout");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_data_timout,
+               test_server_data_timout, 1);
+       test_end();
+}
+
+/*
+ * Successful delivery
+ */
+
+/* server */
+
+enum _successful_delivery_state {
+       SUCCESSFUL_DELIVERY_STATE_EHLO = 0,
+       SUCCESSFUL_DELIVERY_STATE_MAIL_FROM,
+       SUCCESSFUL_DELIVERY_STATE_RCPT_TO,
+       SUCCESSFUL_DELIVERY_STATE_DATA,
+       SUCCESSFUL_DELIVERY_STATE_FINISH
+};
+
+struct _successful_delivery_server {
+       enum _successful_delivery_state state;
+
+       char *file_path;
+       struct istream *dot_input;
+       struct ostream *file;
+};
+
+static void
+test_successful_delivery_input(struct server_connection *conn)
+{
+       struct _successful_delivery_server *ctx;
+       const char *line;
+
+       if (conn->context == NULL) {
+               ctx = p_new(conn->pool, struct _successful_delivery_server, 1);
+               conn->context = (void*)ctx;
+       } else {
+               ctx = (struct _successful_delivery_server *)conn->context;
+       }
+
+       for (;;) {
+               if (ctx->state == SUCCESSFUL_DELIVERY_STATE_FINISH) {
+                       // FIXME: put somewhere common
+                       enum ostream_send_istream_result res;
+
+                       if (ctx->dot_input == NULL) {
+                               int fd;
+
+                               ctx->dot_input = i_stream_create_dot(conn->conn.input, TRUE);
+                               ctx->file_path = p_strdup_printf(conn->pool,
+                                       "%s/message-%u.eml", test_tmp_dir_get(), server_port);
+
+                               if ( (fd=open(ctx->file_path, O_WRONLY | O_CREAT, 0600)) < 0 ) {
+                                       i_fatal("failed create tmp file for message: "
+                                               "open(%s) failed: %m", ctx->file_path);
+                               }
+                               ctx->file = o_stream_create_fd_autoclose(&fd, IO_BLOCK_SIZE);
+                       }
+
+                       res = o_stream_send_istream(ctx->file, ctx->dot_input);
+                       switch (res) {
+                       case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+                               break;
+                       case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+                       case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+                               return;
+                       case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+                               i_error("test server: "
+                                       "Failed to read all message payload [%s]", ctx->file_path);
+                               server_connection_deinit(&conn);
+                               return;
+                       case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+                               i_error("test server: "
+                                       "Failed to write all message payload [%s]", ctx->file_path);
+                               server_connection_deinit(&conn);
+                               return;
+                       }
+
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.0.0 Ok: queued as 73BDE342129\r\n");
+                       ctx->state = SUCCESSFUL_DELIVERY_STATE_MAIL_FROM;
+                       continue;
+               }
+
+               line = i_stream_read_next_line(conn->conn.input);
+               if (line == NULL) {
+                       if (i_stream_is_eof(conn->conn.input) ||
+                               conn->conn.input->stream_errno != 0)
+                               server_connection_deinit(&conn);
+                       return;
+               }
+
+               switch (ctx->state) {
+               case SUCCESSFUL_DELIVERY_STATE_EHLO:
+                       o_stream_send_str(conn->conn.output,
+                               "250-testserver\r\n"
+                               "250-PIPELINING\r\n"
+                               "250-ENHANCEDSTATUSCODES\r\n"
+                               "250-8BITMIME\r\n"
+                               "250 DSN\r\n");
+                       ctx->state = SUCCESSFUL_DELIVERY_STATE_MAIL_FROM;
+                       return;
+               case SUCCESSFUL_DELIVERY_STATE_MAIL_FROM:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.0 Ok\r\n");
+                       ctx->state = SUCCESSFUL_DELIVERY_STATE_RCPT_TO;
+                       continue;
+               case SUCCESSFUL_DELIVERY_STATE_RCPT_TO:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.5 Ok\r\n");
+                       ctx->state = SUCCESSFUL_DELIVERY_STATE_DATA;
+                       continue;
+               case SUCCESSFUL_DELIVERY_STATE_DATA:
+                       o_stream_send_str(conn->conn.output,
+                               "354 End data with <CR><LF>.<CR><LF>\r\n");
+                       ctx->state = SUCCESSFUL_DELIVERY_STATE_FINISH;
+                       continue;
+               case SUCCESSFUL_DELIVERY_STATE_FINISH:
+                       break;
+               }
+               i_unreached();
+       }
+}
+
+static void
+test_successful_delivery_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void
+test_successful_delivery_deinit(struct server_connection *conn)
+{
+       struct _successful_delivery_server *ctx =
+               (struct _successful_delivery_server *)conn->context;
+       if (ctx->dot_input != NULL)
+               i_stream_unref(&ctx->dot_input);
+       if (ctx->file != NULL)
+               o_stream_unref(&ctx->file);
+}
+
+static void test_server_successful_delivery(unsigned int index)
+{
+       test_server_init = test_successful_delivery_init;
+       test_server_input = test_successful_delivery_input;
+       test_server_deinit = test_successful_delivery_deinit;
+       test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_successful_delivery(const struct lda_settings *client_set)
+{
+       const char *error = NULL;
+       int ret;
+
+       /* send the message */
+       ret = test_client_smtp_send_simple_port(client_set,
+               test_message1, bind_ports[0], 5, &error);
+       test_out_reason("run (ret > 0)", ret > 0, error);
+
+       /* verify delivery */
+       test_message_delivery(test_message1,
+               t_strdup_printf("%s/message-%u.eml",
+                       test_tmp_dir_get(), bind_ports[0]));
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_successful_delivery(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("successful delivery");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_successful_delivery,
+               test_server_successful_delivery, 1);
+       test_end();
+}
+
+/*
+ * Failed sendmail
+ */
+
+/* client */
+
+static bool
+test_client_failed_sendmail(const struct lda_settings *client_set)
+{
+       struct lda_settings smtp_client_set;
+       struct smtp_client *smtp_client;
+       struct ostream *output;
+       const char *sendmail_path, *error = NULL;
+       int ret;
+
+       sendmail_path = TEST_BIN_DIR"/sendmail-exit-1.sh";
+
+       smtp_client_set = *client_set;
+       smtp_client_set.sendmail_path = sendmail_path;
+
+       smtp_client = smtp_client_init(&smtp_client_set, "sender@example.com");
+
+       smtp_client_add_rcpt(smtp_client, "rcpt@example.com");
+       output = smtp_client_send(smtp_client);
+       o_stream_send_str(output, test_message1);
+
+       ret = smtp_client_deinit_timeout(smtp_client, 5, &error);
+       test_out_reason("run (ret < 0)", ret < 0, error);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_failed_sendmail(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("failed sendmail");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_failed_sendmail, NULL, 0);
+       test_end();
+}
+
+/*
+ * Successful sendmail
+ */
+
+/* client */
+
+static bool
+test_client_successful_sendmail(const struct lda_settings *client_set)
+{
+       struct lda_settings smtp_client_set;
+       struct smtp_client *smtp_client;
+       struct ostream *output;
+       const char *sendmail_path, *msg_path, *error = NULL;
+       int ret;
+
+       msg_path = t_strdup_printf("%s/message.eml", test_tmp_dir_get());
+
+       sendmail_path = t_strdup_printf(
+               TEST_BIN_DIR"/sendmail-success.sh %s", msg_path);
+
+       smtp_client_set = *client_set;
+       smtp_client_set.sendmail_path = sendmail_path;
+
+       smtp_client = smtp_client_init(&smtp_client_set, "sender@example.com");
+
+       smtp_client_add_rcpt(smtp_client, "rcpt@example.com");
+       output = smtp_client_send(smtp_client);
+       o_stream_send_str(output, test_message1);
+
+       ret = smtp_client_deinit_timeout(smtp_client, 5, &error);
+       test_out_reason("run (ret > 0)", ret > 0, error);
+
+       /* verify delivery */
+       test_message_delivery(test_message1, msg_path);
+
+       return FALSE;
+}
+
+/* test */
+
+static void test_successful_sendmail(void)
+{
+       struct lda_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("successful sendmail");
+       test_expect_errors(0);
+       test_run_client_server(&smtp_client_set,
+               test_client_successful_sendmail, NULL, 0);
+       test_end();
+}
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+       test_host_lookup_failed,
+       test_connection_refused,
+       test_connection_timed_out,
+       test_bad_greeting,
+       test_denied_helo,
+       test_disconnect_helo,
+       test_denied_mail,
+       test_denied_rcpt,
+       test_denied_second_rcpt,
+       test_denied_data,
+       test_data_failure,
+       test_data_disconnect,
+       test_data_timeout,
+       test_successful_delivery,
+       test_failed_sendmail,
+       test_successful_sendmail,
+       NULL
+};
+
+/*
+ * Test client
+ */
+
+static void
+test_client_defaults(struct lda_settings *smtp_set)
+{
+       i_zero(smtp_set);
+       smtp_set->hostname = "test";
+       smtp_set->submission_host = "";
+       smtp_set->sendmail_path = "/bin/false";
+}
+
+static void test_client_deinit(void)
+{
+}
+
+static int
+test_client_smtp_send_simple(const struct lda_settings *smtp_set,
+       const char *message, const char *host,
+       unsigned int timeout_secs, const char **error_r)
+{
+       struct lda_settings smtp_client_set;
+       struct smtp_client *smtp_client;
+       struct ostream *output;
+
+       /* send the message */
+       smtp_client_set = *smtp_set;
+       smtp_client_set.submission_host = host,
+
+       smtp_client = smtp_client_init(&smtp_client_set, "sender@example.com");
+
+       smtp_client_add_rcpt(smtp_client, "rcpt@example.com");
+       output = smtp_client_send(smtp_client);
+       o_stream_send_str(output, message);
+
+       return smtp_client_deinit_timeout
+               (smtp_client, timeout_secs, error_r);
+}
+
+static int
+test_client_smtp_send_simple_port(const struct lda_settings *smtp_set,
+       const char *message, unsigned int port,
+       unsigned int timeout_secs, const char **error_r)
+{
+       const char *host = t_strdup_printf("127.0.0.1:%u", port);
+
+       return test_client_smtp_send_simple(smtp_set,
+               message, host, timeout_secs, error_r);
+}
+
+/*
+ * Test server
+ */
+
+/* client connection */
+
+static void
+server_connection_input(struct connection *_conn)
+{
+       struct server_connection *conn = (struct server_connection *)_conn;
+
+       test_server_input(conn);
+}
+
+static void
+server_connection_init(int fd)
+{
+       struct server_connection *conn;
+       pool_t pool;
+
+       net_set_nonblock(fd, TRUE);
+
+       pool = pool_alloconly_create("server connection", 256);
+       conn = p_new(pool, struct server_connection, 1);
+       conn->pool = pool;
+
+       connection_init_server
+               (server_conn_list, &conn->conn, "server connection", fd, fd);
+
+       if (test_server_init != NULL)
+               test_server_init(conn);
+}
+
+static void
+server_connection_deinit(struct server_connection **_conn)
+{
+       struct server_connection *conn = *_conn;
+
+       *_conn = NULL;
+
+       if (test_server_deinit != NULL)
+               test_server_deinit(conn);
+
+       connection_deinit(&conn->conn);
+       pool_unref(&conn->pool);
+}
+
+static void
+server_connection_destroy(struct connection *_conn)
+{
+       struct server_connection *conn =
+               (struct server_connection *)_conn;
+
+       server_connection_deinit(&conn);
+}
+
+static void
+server_connection_accept(void *context ATTR_UNUSED)
+{
+       int fd;
+
+       /* accept new client */
+       fd = net_accept(fd_listen, NULL, NULL);
+       if (fd == -1)
+               return;
+       if (fd == -2) {
+               i_fatal("test server: accept() failed: %m");
+       }
+
+       server_connection_init(fd);
+}
+
+/* */
+
+static struct connection_settings server_connection_set = {
+       .input_max_size = (size_t)-1,
+       .output_max_size = (size_t)-1,
+       .client = FALSE
+};
+
+static const struct connection_vfuncs server_connection_vfuncs = {
+       .destroy = server_connection_destroy,
+       .input = server_connection_input
+};
+
+static void test_server_run(unsigned int index)
+{
+       server_index = index;
+
+       /* open server socket */
+       io_listen = io_add(fd_listen,
+               IO_READ, server_connection_accept, (void *)NULL);
+
+       server_conn_list = connection_list_init
+               (&server_connection_set, &server_connection_vfuncs);
+
+       io_loop_run(ioloop);
+
+       /* close server socket */
+       io_remove(&io_listen);
+
+       connection_list_deinit(&server_conn_list);
+}
+
+/*
+ * Tests
+ */
+
+static int test_open_server_fd(in_port_t *bind_port)
+{
+       int fd = net_listen(&bind_ip, bind_port, 128);
+       if (debug)
+               i_debug("server listening on %u", *bind_port);
+       if (fd == -1) {
+               i_fatal("listen(%s:%u) failed: %m",
+                       net_ip2addr(&bind_ip), *bind_port);
+       }
+       return fd;
+}
+
+static void test_servers_kill_all(void)
+{
+       unsigned int i;
+
+       if (server_pids_count > 0) {
+               for (i = 0; i < server_pids_count; i++) {
+                       if (server_pids[i] != (pid_t)-1) {
+                               (void)kill(server_pids[i], SIGKILL);
+                               (void)waitpid(server_pids[i], NULL, 0);
+                               server_pids[i] = -1;
+                       }
+               }
+       }
+       server_pids_count = 0;
+}
+
+static void test_tmp_dir_init(void)
+{
+       tmp_dir = i_strdup_printf
+               ("/tmp/dovecot-test-smtp-client.%s.%s",
+                       dec2str(time(NULL)), dec2str(getpid()));
+}
+
+static const char *test_tmp_dir_get(void)
+{
+       if (mkdir(tmp_dir, 0700) < 0 && errno != EEXIST) {
+               i_fatal("failed to create temporary directory `%s': %m",
+                       tmp_dir);
+       }
+       return tmp_dir;
+}
+
+static void test_tmp_dir_deinit(void)
+{
+       const char *error;
+
+       if (unlink_directory(tmp_dir,
+               UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0 && errno != ENOENT) {
+               i_warning("failed to remove temporary directory `%s': %s.",
+                       tmp_dir, error);
+       }
+
+       i_free(tmp_dir);
+}
+
+static void
+test_message_delivery(const char *message, const char *file)
+{
+       struct istream *input;
+       const unsigned char *data;
+       size_t size, msize;
+       int ret;
+
+       msize = strlen(message);
+
+       input = i_stream_create_file(file, (size_t)-1);
+       while ((ret=i_stream_read_more(input, &data, &size)) > 0) {
+               const unsigned char *mdata;
+               if (input->v_offset >= (uoff_t)msize ||
+                       (input->v_offset + (uoff_t)size) > (uoff_t)msize) {
+                       ret = -1;
+                       break;
+               }
+               mdata = (const unsigned char *)message + input->v_offset;
+               if (memcmp(data, mdata, size) != 0) {
+                       ret = -1;
+                       break;
+               }
+               i_stream_skip(input, size);
+       }
+
+       test_out_reason("delivery",
+               input->stream_errno == 0 &&
+               i_stream_is_eof(input) &&
+               input->v_offset == (uoff_t)msize,
+               (input->stream_errno == 0 ? NULL : i_stream_get_error(input)));
+       i_stream_unref(&input);
+}
+
+static void test_run_client_server(
+       const struct lda_settings *client_set,
+       test_client_init_t client_test,
+       test_server_init_t server_test,
+       unsigned int server_tests_count)
+{
+       unsigned int i;
+
+       server_pids = NULL;
+       server_pids_count = 0;
+
+       test_tmp_dir_init();
+
+       if (server_tests_count > 0) {
+               int fds[server_tests_count];
+
+               bind_ports = i_new(in_port_t, server_tests_count);
+
+               lib_signals_ioloop_detach();
+
+               server_pids = i_new(pid_t, server_tests_count);
+               for (i = 0; i < server_tests_count; i++)
+                       server_pids[i] = (pid_t)-1;
+               server_pids_count = server_tests_count;
+
+               for (i = 0; i < server_tests_count; i++)
+                       fds[i] = test_open_server_fd(&bind_ports[i]);
+
+               for (i = 0; i < server_tests_count; i++) {
+                       fd_listen = fds[i];
+                       server_port = bind_ports[i];
+                       if ((server_pids[i] = fork()) == (pid_t)-1)
+                               i_fatal("fork() failed: %m");
+                       if (server_pids[i] == 0) {
+                               server_pids[i] = (pid_t)-1;
+                               server_pids_count = 0;
+                               hostpid_init();
+                               while (current_ioloop != NULL) {
+                                       ioloop = current_ioloop;
+                                       io_loop_destroy(&ioloop);
+                               }
+                               lib_signals_deinit();
+                               if (debug)
+                                       i_debug("server[%d]: PID=%s", i+1, my_pid);
+                               /* child: server */
+                               ioloop = io_loop_create();
+                               server_test(i);
+                               io_loop_destroy(&ioloop);
+                               if (fd_listen != -1)
+                                       i_close_fd(&fd_listen);
+                               i_free(bind_ports);
+                               i_free(server_pids);
+                               test_tmp_dir_deinit();
+                               /* wait for it to be killed; this way, valgrind will not
+                                  object to this process going away inelegantly. */
+                               sleep(60);
+                               exit(1);
+                       }
+                       if (fd_listen != -1)
+                               i_close_fd(&fd_listen);
+               }
+               if (debug)
+                       i_debug("client: PID=%s", my_pid);
+
+               lib_signals_ioloop_attach();
+       }
+
+       /* parent: client */
+
+       usleep(100000); /* wait a little for server setup */
+       server_port = 0;
+
+       ioloop = io_loop_create();
+       if (client_test(client_set))
+               io_loop_run(ioloop);
+       test_client_deinit();
+       io_loop_destroy(&ioloop);
+
+       test_servers_kill_all();
+       i_free(server_pids);
+       i_free(bind_ports);
+       test_tmp_dir_deinit();
+}
+
+/*
+ * Main
+ */
+
+volatile sig_atomic_t terminating = 0;
+
+static void
+test_signal_handler(int signo)
+{
+       if (terminating != 0)
+               raise(signo);
+       terminating = 1;
+
+       /* make sure we don't leave any pesky children alive */
+       test_servers_kill_all();
+
+       (void)signal(signo, SIG_DFL);
+       raise(signo);
+}
+
+static void test_atexit(void)
+{
+       test_servers_kill_all();
+}
+
+int main(int argc, char *argv[])
+{
+       int c;
+
+       atexit(test_atexit);
+       (void)signal(SIGCHLD, SIG_IGN);
+       (void)signal(SIGTERM, test_signal_handler);
+       (void)signal(SIGQUIT, test_signal_handler);
+       (void)signal(SIGINT, test_signal_handler);
+       (void)signal(SIGSEGV, test_signal_handler);
+       (void)signal(SIGABRT, test_signal_handler);
+
+       master_service = master_service_init("test-smtp-client",
+               MASTER_SERVICE_FLAG_STANDALONE, &argc, &argv, "D");
+
+       while ((c = master_getopt(master_service)) > 0) {
+               switch (c) {
+               case 'D':
+                       debug = TRUE;
+                       break;
+               default:
+                       i_fatal("Usage: %s [-D]", argv[0]);
+               }
+       }
+
+       master_service_init_finish(master_service);
+
+       /* listen on localhost */
+       i_zero(&bind_ip);
+       bind_ip.family = AF_INET;
+       bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+       test_run(test_functions);
+
+       master_service_deinit(&master_service);
+}