]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-smtp: Implemented test suite for client errors.
authorStephan Bosch <stephan.bosch@dovecot.fi>
Sun, 29 Oct 2017 17:36:25 +0000 (18:36 +0100)
committerTimo Sirainen <timo.sirainen@dovecot.fi>
Sun, 26 Nov 2017 20:30:01 +0000 (22:30 +0200)
src/lib-smtp/Makefile.am
src/lib-smtp/test-smtp-client-errors.c [new file with mode: 0644]

index 79f958b3813038a30b3fd87427381f57ae869a9e..345550a208190b9186c6d0a218f7bf98761728a2 100644 (file)
@@ -52,7 +52,8 @@ test_programs = \
        test-smtp-reply-parser
 
 test_nocheck_programs = \
-       test-smtp-submit
+       test-smtp-submit \
+       test-smtp-client-errors
 
 noinst_PROGRAMS = $(test_programs) $(test_nocheck_programs)
 
@@ -103,6 +104,11 @@ test_smtp_submit_LDFLAGS = -export-dynamic
 test_smtp_submit_LDADD = $(test_libs)
 test_smtp_submit_DEPENDENCIES = $(test_deps)
 
+test_smtp_client_errors_SOURCES = test-smtp-client-errors.c
+test_smtp_client_errors_LDFLAGS = -export-dynamic
+test_smtp_client_errors_LDADD = $(test_libs)
+test_smtp_client_errors_DEPENDENCIES = $(test_deps)
+
 check-local:
        for bin in $(test_programs); do \
          if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
diff --git a/src/lib-smtp/test-smtp-client-errors.c b/src/lib-smtp/test-smtp-client-errors.c
new file mode 100644 (file)
index 0000000..28e59d6
--- /dev/null
@@ -0,0 +1,2969 @@
+/* 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-dot.h"
+#include "istream-chain.h"
+#include "istream-failure-at.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "connection.h"
+#include "test-common.h"
+#include "smtp-client.h"
+#include "smtp-client-connection.h"
+#include "smtp-client-transaction.h"
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <signal.h>
+#include <unistd.h>
+
+/*
+ * Types
+ */
+
+enum server_connection_state {
+       SERVER_CONNECTION_STATE_EHLO = 0,
+       SERVER_CONNECTION_STATE_MAIL_FROM,
+       SERVER_CONNECTION_STATE_RCPT_TO,
+       SERVER_CONNECTION_STATE_DATA,
+       SERVER_CONNECTION_STATE_FINISH
+};
+
+struct server_connection {
+       struct connection conn;
+       void *context;
+
+       enum server_connection_state state;
+       char *file_path;
+       struct istream *dot_input;
+
+       pool_t pool;
+};
+
+typedef void (*test_server_init_t)(unsigned int index);
+typedef bool (*test_client_init_t)
+       (const struct smtp_client_settings *client_set);
+typedef void (*test_dns_init_t)(void);
+
+/*
+ * State
+ */
+
+/* common */
+static struct ip_addr bind_ip;
+static in_port_t *bind_ports = 0;
+static struct ioloop *ioloop;
+static bool debug = FALSE;
+
+/* dns */
+static pid_t dns_pid = (pid_t)-1;
+
+/* server */
+static struct io *io_listen;
+static int fd_listen = -1;
+static pid_t *server_pids = NULL;
+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 int (*test_server_input_line)(struct server_connection *conn,
+       const char *line);
+static int (*test_server_input_data)(struct server_connection *conn,
+       const unsigned char *data, size_t size);
+static int (*test_server_init)(struct server_connection *conn);
+static void (*test_server_deinit)(struct server_connection *conn);
+
+/* client */
+static struct smtp_client *smtp_client = NULL;
+
+/*
+ * 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 smtp_client_settings *smtp_set);
+static void test_client_deinit(void);
+
+/* test*/
+static void test_run_client_server(
+       const struct smtp_client_settings *client_set,
+       test_client_init_t client_test,
+       test_server_init_t server_test,
+       unsigned int server_tests_count,
+       test_dns_init_t dns_test)
+       ATTR_NULL(3);
+
+/*
+ * Unconfigured SSL
+ */
+
+/* server */
+
+static void
+test_server_unconfigured_ssl_input(
+       struct server_connection *conn ATTR_UNUSED)
+{
+       /* nothing */
+}
+
+static void
+test_server_unconfigured_ssl(unsigned int index)
+{
+       sleep(100);
+       test_server_input = test_server_unconfigured_ssl_input;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _unconfigured_ssl {
+       unsigned int count;
+};
+
+static void
+test_client_unconfigured_ssl_reply(const struct smtp_reply *reply,
+       void *context)
+{
+       struct _unconfigured_ssl *ctx = (struct _unconfigured_ssl *)context;
+
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED);
+
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_unconfigured_ssl(const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct _unconfigured_ssl *ctx;
+
+       test_expect_errors(2);
+
+       ctx = i_new(struct _unconfigured_ssl, 1);
+       ctx->count = 2;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "127.0.0.1", bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn,
+               test_client_unconfigured_ssl_reply, ctx);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "127.0.0.1", bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn,
+               test_client_unconfigured_ssl_reply, ctx);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_unconfigured_ssl(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("unconfigured ssl");
+       test_run_client_server(&smtp_client_set,
+               test_client_unconfigured_ssl,
+               test_server_unconfigured_ssl, 1, NULL);
+       test_end();
+}
+
+/*
+ * Unconfigured SSL abort
+ */
+
+/* server */
+
+static void
+test_server_unconfigured_ssl_abort_input(
+       struct server_connection *conn ATTR_UNUSED)
+{
+       /* nothing */
+}
+
+static void
+test_server_unconfigured_ssl_abort(unsigned int index)
+{
+       sleep(100);
+       test_server_input = test_server_unconfigured_ssl_abort_input;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _unconfigured_ssl_abort {
+       unsigned int count;
+};
+
+static void
+test_client_unconfigured_ssl_abort_reply1(const struct smtp_reply *reply,
+       struct _unconfigured_ssl_abort *ctx ATTR_UNUSED)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_out_quiet("inappropriate callback", FALSE);
+}
+
+static void
+test_client_unconfigured_ssl_abort_reply2(const struct smtp_reply *reply,
+       struct _unconfigured_ssl_abort *ctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED);
+
+       i_free(ctx);
+       io_loop_stop(ioloop);
+}
+
+static bool
+test_client_unconfigured_ssl_abort(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _unconfigured_ssl_abort *ctx;
+
+       test_expect_errors(2);
+
+       ctx = i_new(struct _unconfigured_ssl_abort, 1);
+       ctx->count = 1;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "127.0.0.1", bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_unconfigured_ssl_abort_reply1, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+       smtp_client_command_abort(&scmd);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "127.0.0.1", bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_unconfigured_ssl_abort_reply2, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_unconfigured_ssl_abort(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("unconfigured ssl abort");
+       test_run_client_server(&smtp_client_set,
+               test_client_unconfigured_ssl_abort,
+               test_server_unconfigured_ssl_abort, 1, NULL);
+       test_end();
+}
+
+/*
+ * Host lookup failed
+ */
+
+/* client */
+
+struct _host_lookup_failed {
+       unsigned int count;
+};
+
+static void
+test_client_host_lookup_failed_reply(const struct smtp_reply *reply,
+       struct _host_lookup_failed *ctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED);
+
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_host_lookup_failed(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _host_lookup_failed *ctx;
+
+       test_expect_errors(2);
+
+       ctx = i_new(struct _host_lookup_failed, 1);
+       ctx->count = 2;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "host.in-addr.arpa", 465,
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_host_lookup_failed_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "host.in-addr.arpa", 465,
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_host_lookup_failed_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_host_lookup_failed(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("host lookup failed");
+       test_run_client_server(&smtp_client_set,
+               test_client_host_lookup_failed,
+               NULL, 0, NULL);
+       test_end();
+}
+
+/*
+ * Connection refused
+ */
+
+/* server */
+
+static void
+test_server_connection_refused(unsigned int index ATTR_UNUSED)
+{
+       i_close_fd(&fd_listen);
+}
+
+/* client */
+
+struct _connection_refused {
+       unsigned int count;
+};
+
+static void
+test_client_connection_refused_reply(const struct smtp_reply *reply,
+       struct _connection_refused *ctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED);
+
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_connection_refused(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _connection_refused *ctx;
+
+       test_expect_errors(2);
+
+       ctx = i_new(struct _connection_refused, 1);
+       ctx->count = 2;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_connection_refused_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_connection_refused_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_connection_refused(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("connection refused");
+       test_run_client_server(&smtp_client_set,
+               test_client_connection_refused,
+               test_server_connection_refused, 1, NULL);
+       test_end();
+}
+
+/*
+ * Connection lost prematurely
+ */
+
+/* server */
+
+static void
+test_connection_lost_prematurely_input(struct server_connection *conn)
+{
+       const char *line;
+
+       line = i_stream_read_next_line(conn->conn.input);
+       if (line == NULL) {
+               if (conn->conn.input->eof ||
+                       conn->conn.input->stream_errno != 0) {
+                       server_connection_deinit(&conn);
+               }
+               return;
+       }
+       server_connection_deinit(&conn);
+}
+
+static int
+test_connection_lost_prematurely_init(struct server_connection *conn)
+{
+       o_stream_send_str(conn->conn.output,
+               "220 testserver ESMTP Testfix (Frop/GNU)\r\n");
+       return 1;
+}
+
+static void test_server_connection_lost_prematurely(unsigned int index)
+{
+       test_server_init = test_connection_lost_prematurely_init;
+       test_server_input = test_connection_lost_prematurely_input;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost_prematurely {
+       unsigned int count;
+};
+
+static void
+test_client_connection_lost_prematurely_reply(const struct smtp_reply *reply,
+       struct _connection_lost_prematurely *ctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_connection_lost_prematurely(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _connection_lost_prematurely *ctx;
+
+       ctx = i_new(struct _connection_lost_prematurely, 1);
+       ctx->count = 2;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_connection_lost_prematurely_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_connection_lost_prematurely_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost_prematurely(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("connection lost prematurely");
+       test_run_client_server(&smtp_client_set,
+               test_client_connection_lost_prematurely,
+               test_server_connection_lost_prematurely, 1, NULL);
+       test_end();
+}
+
+/*
+ * Connection timed out
+ */
+
+/* server */
+
+static void test_server_connection_timed_out(unsigned int index ATTR_UNUSED)
+{
+       sleep(10);
+}
+
+/* client */
+
+struct _connection_timed_out {
+       unsigned int count;
+};
+
+static void
+test_client_connection_timed_out_reply(const struct smtp_reply *reply,
+       struct _connection_timed_out *ctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED);
+
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_connection_timed_out(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _connection_timed_out *ctx;
+
+       test_expect_errors(2);
+
+       ctx = i_new(struct _connection_timed_out, 1);
+       ctx->count = 2;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_connection_timed_out_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_connection_timed_out_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_connection_timed_out(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+       smtp_client_set.connect_timeout_msecs = 1000;
+
+       test_begin("connection timed out");
+       test_run_client_server(&smtp_client_set,
+               test_client_connection_timed_out,
+               test_server_connection_timed_out, 1, NULL);
+       test_end();
+}
+
+/*
+ * Broken payload
+ */
+
+/* server */
+
+static int
+test_broken_payload_input_line(struct server_connection *conn ATTR_UNUSED,
+       const char *line ATTR_UNUSED)
+{
+       return 0;
+}
+
+static void test_server_broken_payload(unsigned int index)
+{
+       test_server_input_line = test_broken_payload_input_line;
+       test_server_run(index);
+}
+
+static int
+test_broken_payload_chunking_input_line(
+       struct server_connection *conn, const char *line ATTR_UNUSED)
+{
+       if (conn->state == SERVER_CONNECTION_STATE_EHLO) {
+               o_stream_send_str(conn->conn.output,
+                       "250-testserver\r\n"
+                       "250-PIPELINING\r\n"
+                       "250-CHUNKING\r\n"
+                       "250-ENHANCEDSTATUSCODES\r\n"
+                       "250 DSN\r\n");
+               return 1;
+       }
+       return 0;
+}
+
+static void test_server_broken_payload_chunking(unsigned int index)
+{
+       test_server_input_line = test_broken_payload_chunking_input_line;
+       test_server_run(index);
+}
+
+/* client */
+
+static void
+test_client_broken_payload_rcpt_to_cb(const struct smtp_reply *reply,
+       void *context ATTR_UNUSED)
+{
+       test_assert(smtp_reply_is_success(reply));
+}
+
+static void
+test_client_broken_payload_rcpt_data_cb(const struct smtp_reply *reply,
+       void *context ATTR_UNUSED)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_BROKEN_PAYLOAD);
+}
+
+static void
+test_client_broken_payload_data_cb(const struct smtp_reply *reply,
+       void *context ATTR_UNUSED)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_BROKEN_PAYLOAD);
+}
+
+static void
+test_client_broken_payload_finished(void *context ATTR_UNUSED)
+{
+       io_loop_stop(ioloop);
+}
+
+static bool
+test_client_broken_payload(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_transaction *strans;
+       struct istream *input;
+
+       test_expect_errors(2);
+
+       input = i_stream_create_error_str(EIO, "Moehahahaha!!");
+       i_stream_set_name(input, "PURE EVIL");
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       strans = smtp_client_transaction_create(sconn,
+               SMTP_ADDRESS_LITERAL("sender", "example.com"), NULL,
+               test_client_broken_payload_finished, NULL);
+       smtp_client_connection_unref(&sconn);
+
+       smtp_client_transaction_add_rcpt(strans,
+               SMTP_ADDRESS_LITERAL("rcpt", "example.com"), NULL,
+               test_client_broken_payload_rcpt_to_cb,
+               test_client_broken_payload_rcpt_data_cb, NULL);
+       smtp_client_transaction_send(strans, input,
+               test_client_broken_payload_data_cb, NULL);
+       i_stream_unref(&input);
+
+       return TRUE;
+}
+
+static bool
+test_client_broken_payload_later(
+       const struct smtp_client_settings *client_set)
+{
+       static const char *message =
+               "From: lucifer@example.com\r\n"
+               "To: lostsoul@example.com\r\n"
+               "Subject: Moehahaha!\r\n"
+               "\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+               "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n";
+       struct smtp_client_connection *sconn;
+       struct smtp_client_transaction *strans;
+       struct istream *input, *msg_input;
+
+       test_expect_errors(1);
+
+       msg_input = i_stream_create_from_data(message, strlen(message));
+       input = i_stream_create_failure_at(msg_input, 666, "Moehahahaha!!");
+       i_stream_unref(&msg_input);
+       i_stream_set_name(input, "PURE EVIL");
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       strans = smtp_client_transaction_create(sconn,
+               SMTP_ADDRESS_LITERAL("sender", "example.com"), NULL,
+               test_client_broken_payload_finished, NULL);
+       smtp_client_connection_unref(&sconn);
+
+       smtp_client_transaction_add_rcpt(strans,
+               SMTP_ADDRESS_LITERAL("rcpt", "example.com"), NULL,
+               test_client_broken_payload_rcpt_to_cb,
+               test_client_broken_payload_rcpt_data_cb, NULL);
+       smtp_client_transaction_send
+               (strans, input, test_client_broken_payload_data_cb, NULL);
+       i_stream_unref(&input);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_broken_payload(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+       smtp_client_set.connect_timeout_msecs = 1000;
+
+       test_begin("broken payload");
+       test_run_client_server(&smtp_client_set,
+               test_client_broken_payload,
+               test_server_broken_payload, 1, NULL);
+       test_end();
+
+       test_begin("broken payload (later)");
+       test_run_client_server(&smtp_client_set,
+               test_client_broken_payload_later,
+               test_server_broken_payload, 1, NULL);
+       test_end();
+
+       test_begin("broken payload (later, chunking)");
+       test_run_client_server(&smtp_client_set,
+               test_client_broken_payload_later,
+               test_server_broken_payload_chunking, 1, NULL);
+       test_end();
+}
+
+/*
+ * Connection lost
+ */
+
+/* server */
+
+static int
+test_connection_lost_input_line(struct server_connection *conn,
+       const char *line ATTR_UNUSED)
+{
+       switch (conn->state) {
+       case SERVER_CONNECTION_STATE_EHLO:
+               if (server_index == 0) {
+                       conn->state = SERVER_CONNECTION_STATE_MAIL_FROM;
+                       sleep(1);
+                       server_connection_deinit(&conn);
+                       return -1;
+               }
+               break;
+       case SERVER_CONNECTION_STATE_MAIL_FROM:
+               if (server_index == 1) {
+                       conn->state = SERVER_CONNECTION_STATE_RCPT_TO;
+                       sleep(1);
+                       server_connection_deinit(&conn);
+                       return -1;
+               }
+               break;
+       case SERVER_CONNECTION_STATE_RCPT_TO:
+               if (server_index == 2) {
+                       conn->state = SERVER_CONNECTION_STATE_DATA;
+                       sleep(1);
+                       server_connection_deinit(&conn);
+                       return -1;
+               }
+               break;
+       case SERVER_CONNECTION_STATE_DATA:
+               if (server_index == 3) {
+                       conn->state = SERVER_CONNECTION_STATE_FINISH;
+                       sleep(1);
+                       server_connection_deinit(&conn);
+                       return -1;
+               }
+               break;
+       case SERVER_CONNECTION_STATE_FINISH:
+               break;
+       }
+       return 0;
+}
+
+static int
+test_connection_lost_input_data(struct server_connection *conn,
+       const unsigned char *data ATTR_UNUSED, size_t size ATTR_UNUSED)
+{
+       sleep(1);
+       server_connection_deinit(&conn);
+       return -1;
+}
+
+static void test_server_connection_lost(unsigned int index)
+{
+       test_server_input_line = test_connection_lost_input_line;
+       test_server_input_data = test_connection_lost_input_data;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost {
+       unsigned int count;
+};
+
+struct _connection_lost_peer {
+       struct _connection_lost *context;
+       unsigned int index;
+};
+
+static void
+test_client_connection_lost_rcpt_to_cb(const struct smtp_reply *reply,
+       struct _connection_lost_peer *pctx)
+{
+       if (debug) {
+               i_debug("RCPT TO REPLY[%u]: %s",
+                       pctx->index, smtp_reply_log(reply));
+       }
+
+       switch (pctx->index) {
+       case 0:
+               test_assert(reply->status ==
+                       SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+               break;
+       case 1:
+               test_assert(reply->status ==
+                       SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+               break;
+       case 2:
+               test_assert(reply->status ==
+                       SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+               break;
+       case 3:
+               test_assert(smtp_reply_is_success(reply));
+               break;
+       }
+}
+
+static void
+test_client_connection_lost_rcpt_data_cb(const struct smtp_reply *reply,
+       struct _connection_lost_peer *pctx)
+{
+       if (debug) {
+               i_debug("RCPT DATA REPLY[%u]: %s",
+                       pctx->index, smtp_reply_log(reply));
+       }
+
+       switch (pctx->index) {
+       case 0:
+               test_assert(FALSE);
+               break;
+       case 1:
+               test_assert(FALSE);
+               break;
+       case 2:
+               test_assert(FALSE);
+               break;
+       case 3:
+               test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+               break;
+       }
+}
+
+static void
+test_client_connection_lost_data_cb(const struct smtp_reply *reply,
+       struct _connection_lost_peer *pctx)
+{
+       if (debug) {
+               i_debug("DATA REPLY[%u]: %s",
+                       pctx->index, smtp_reply_log(reply));
+       }
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+}
+
+static void
+test_client_connection_lost_finished(struct _connection_lost_peer *pctx)
+{
+       struct _connection_lost *ctx = pctx->context;
+
+       if (debug)
+               i_debug("FINISHED[%u]", pctx->index);
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+       i_free(pctx);
+}
+
+static void
+test_client_connection_lost_submit(struct _connection_lost *ctx,
+       unsigned int index)
+{
+       static const char *message =
+               "From: stephan@example.com\r\n"
+               "To: timo@example.com\r\n"
+               "Subject: Frop!\r\n"
+               "\r\n"
+               "Frop!\r\n";
+       struct _connection_lost_peer *pctx;
+       struct smtp_client_connection *sconn;
+       struct smtp_client_transaction *strans;
+       struct istream *input;
+
+       pctx = i_new(struct _connection_lost_peer, 1);
+       pctx->context = ctx;
+       pctx->index = index;
+
+       input = i_stream_create_from_data(message, strlen(message));
+       i_stream_set_name(input, "message");
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[index],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       strans = smtp_client_transaction_create(sconn,
+               SMTP_ADDRESS_LITERAL("sender", "example.com"), NULL,
+               test_client_connection_lost_finished, pctx);
+       smtp_client_connection_unref(&sconn);
+
+       smtp_client_transaction_add_rcpt(strans,
+               SMTP_ADDRESS_LITERAL("rcpt", "example.com"), NULL,
+               test_client_connection_lost_rcpt_to_cb,
+               test_client_connection_lost_rcpt_data_cb, pctx);
+       smtp_client_transaction_send
+               (strans, input, test_client_connection_lost_data_cb, pctx);
+       i_stream_unref(&input);
+}
+
+static bool
+test_client_connection_lost(
+       const struct smtp_client_settings *client_set)
+{
+       struct _connection_lost *ctx;
+       unsigned int i;
+
+       ctx = i_new(struct _connection_lost, 1);
+       ctx->count = 5;
+
+       smtp_client = smtp_client_init(client_set);
+
+       for (i = 0; i < ctx->count; i++)
+               test_client_connection_lost_submit(ctx, i);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("connection lost");
+       test_run_client_server(&smtp_client_set,
+               test_client_connection_lost,
+               test_server_connection_lost, 5, NULL);
+       test_end();
+}
+
+/*
+ * Unexpected reply
+ */
+
+/* server */
+
+static int
+test_unexpected_reply_init(struct server_connection *conn)
+{
+       if (server_index == 5) {
+               o_stream_send_str(conn->conn.output,
+                       "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+               o_stream_send_str(conn->conn.output,
+                       "421 testserver Server shutting down for maintenance\r\n");
+               sleep(4);
+               server_connection_deinit(&conn);
+               return 1;
+       }
+       return 0;
+}
+
+static int
+test_unexpected_reply_input_line(struct server_connection *conn,
+       const char *line ATTR_UNUSED)
+{
+       switch (conn->state) {
+       case SERVER_CONNECTION_STATE_EHLO:
+               if (server_index == 4) {
+                       o_stream_send_str(conn->conn.output,
+                               "250-testserver\r\n"
+                               "250-PIPELINING\r\n"
+                               "250-ENHANCEDSTATUSCODES\r\n"
+                               "250 DSN\r\n");
+                       o_stream_send_str(conn->conn.output,
+                               "421 testserver Server shutting down for maintenance\r\n");
+                       sleep(4);
+                       server_connection_deinit(&conn);
+                       return -1;
+               }
+               break;
+       case SERVER_CONNECTION_STATE_MAIL_FROM:
+               if (server_index == 3) {
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.0 Ok\r\n");
+                       o_stream_send_str(conn->conn.output,
+                               "421 testserver Server shutting down for maintenance\r\n");
+                       sleep(4);
+                       server_connection_deinit(&conn);
+                       return -1;
+               }
+               break;
+       case SERVER_CONNECTION_STATE_RCPT_TO:
+               if (server_index == 2) {
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.5 Ok\r\n");
+                       o_stream_send_str(conn->conn.output,
+                               "421 testserver Server shutting down for maintenance\r\n");
+                       sleep(4);
+                       server_connection_deinit(&conn);
+                       return -1;
+               }
+               break;
+       case SERVER_CONNECTION_STATE_DATA:
+               if (server_index == 1) {
+                       o_stream_send_str(conn->conn.output,
+                               "354 End data with <CR><LF>.<CR><LF>\r\n");
+                       o_stream_send_str(conn->conn.output,
+                               "421 testserver Server shutting down for maintenance\r\n");
+                       sleep(4);
+                       server_connection_deinit(&conn);
+                       return -1;
+               }
+               break;
+       case SERVER_CONNECTION_STATE_FINISH:
+               break;
+       }
+       return 0;
+}
+
+static void test_server_unexpected_reply(unsigned int index)
+{
+       test_server_init = test_unexpected_reply_init;
+       test_server_input_line = test_unexpected_reply_input_line;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _unexpected_reply {
+       unsigned int count;
+};
+
+struct _unexpected_reply_peer {
+       struct _unexpected_reply *context;
+       unsigned int index;
+
+       struct smtp_client_connection *conn;
+       struct smtp_client_transaction *trans;
+       struct timeout *to;
+
+       bool login_callback:1;
+       bool mail_from_callback:1;
+       bool rcpt_to_callback:1;
+       bool rcpt_data_callback:1;
+       bool data_callback:1;
+};
+
+static void
+test_client_unexpected_reply_login_cb(const struct smtp_reply *reply,
+       void *context)
+{
+       struct _unexpected_reply_peer *pctx =
+               (struct _unexpected_reply_peer *)context;
+
+       pctx->login_callback = TRUE;
+
+       if (debug)
+               i_debug("LOGIN REPLY[%u]: %s", pctx->index, smtp_reply_log(reply));
+
+       switch (pctx->index) {
+       case 0: case 1: case 2: case 3: case 4:
+               test_assert(smtp_reply_is_success(reply));
+               break;
+       case 5:
+               test_assert(reply->status == 421);
+               break;
+       }
+}
+
+static void
+test_client_unexpected_reply_mail_from_cb(const struct smtp_reply *reply,
+       struct _unexpected_reply_peer *pctx)
+{
+       if (debug)
+               i_debug("MAIL FROM REPLY[%u]: %s", pctx->index, smtp_reply_log(reply));
+
+       pctx->mail_from_callback = TRUE;
+
+       switch (pctx->index) {
+       case 0: case 1: case 2: case 3:
+               test_assert(smtp_reply_is_success(reply));
+               break;
+       case 4: case 5:
+               test_assert(reply->status == 421);
+               break;
+       }
+}
+
+static void
+test_client_unexpected_reply_rcpt_to_cb(const struct smtp_reply *reply,
+       struct _unexpected_reply_peer *pctx)
+{
+       if (debug)
+               i_debug("RCPT TO REPLY[%u]: %s", pctx->index, smtp_reply_log(reply));
+
+       pctx->rcpt_to_callback = TRUE;
+
+       switch (pctx->index) {
+       case 0: case 1: case 2:
+               test_assert(smtp_reply_is_success(reply));
+               break;
+       case 3: case 4: case 5:
+               test_assert(reply->status == 421);
+               break;
+       }
+}
+
+static void
+test_client_unexpected_reply_rcpt_data_cb(const struct smtp_reply *reply,
+       struct _unexpected_reply_peer *pctx)
+{
+       if (debug)
+               i_debug("RCPT DATA REPLY[%u]: %s", pctx->index, smtp_reply_log(reply));
+
+       pctx->rcpt_data_callback = TRUE;
+
+       switch (pctx->index) {
+       case 0:
+               test_assert(smtp_reply_is_success(reply));
+               break;
+       case 1: case 2:
+               test_assert(reply->status == 421);
+               break;
+       case 3: case 4: case 5:
+               i_unreached();
+       }
+}
+
+static void
+test_client_unexpected_reply_data_cb(const struct smtp_reply *reply,
+       struct _unexpected_reply_peer *pctx)
+{
+       if (debug)
+               i_debug("DATA REPLY[%u]: %s", pctx->index, smtp_reply_log(reply));
+
+       pctx->data_callback = TRUE;
+
+       switch (pctx->index) {
+       case 0:
+               test_assert(smtp_reply_is_success(reply));
+               break;
+       case 1: case 2: case 3: case 4: case 5:
+               test_assert(reply->status == 421);
+               break;
+       }
+}
+
+static void
+test_client_unexpected_reply_finished(struct _unexpected_reply_peer *pctx)
+{
+       struct _unexpected_reply *ctx = pctx->context;
+
+       if (debug)
+               i_debug("FINISHED[%u]", pctx->index);
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+
+       switch (pctx->index) {
+       case 0: case 1: case 2:
+               test_assert(pctx->mail_from_callback);
+               test_assert(pctx->rcpt_to_callback);
+               test_assert(pctx->rcpt_data_callback);
+               test_assert(pctx->data_callback);
+               break;
+       case 3: case 4: case 5:
+               test_assert(pctx->mail_from_callback);
+               test_assert(pctx->rcpt_to_callback);
+               test_assert(!pctx->rcpt_data_callback);
+               test_assert(pctx->data_callback);
+               break;
+       }
+
+       pctx->trans = NULL;
+       timeout_remove(&pctx->to);
+       i_free(pctx);
+}
+
+static void
+test_client_unexpected_reply_submit2(struct _unexpected_reply_peer *pctx)
+{
+       struct smtp_client_transaction *strans = pctx->trans;
+       static const char *message =
+               "From: stephan@example.com\r\n"
+               "To: timo@example.com\r\n"
+               "Subject: Frop!\r\n"
+               "\r\n"
+               "Frop!\r\n";
+       struct istream *input;
+
+       timeout_remove(&pctx->to);
+
+       input = i_stream_create_from_data(message, strlen(message));
+       i_stream_set_name(input, "message");
+
+       smtp_client_transaction_send
+               (strans, input, test_client_unexpected_reply_data_cb, pctx);
+       i_stream_unref(&input);
+}
+
+static void
+test_client_unexpected_reply_submit1(struct _unexpected_reply_peer *pctx)
+{
+       timeout_remove(&pctx->to);
+
+       smtp_client_transaction_add_rcpt(pctx->trans,
+               SMTP_ADDRESS_LITERAL("rcpt", "example.com"), NULL,
+               test_client_unexpected_reply_rcpt_to_cb,
+               test_client_unexpected_reply_rcpt_data_cb, pctx);
+
+       pctx->to = timeout_add_short(500,
+               test_client_unexpected_reply_submit2, pctx);
+}
+
+static void
+test_client_unexpected_reply_submit(struct _unexpected_reply *ctx,
+       unsigned int index)
+{
+       struct _unexpected_reply_peer *pctx;
+
+       pctx = i_new(struct _unexpected_reply_peer, 1);
+       pctx->context = ctx;
+       pctx->index = index;
+
+       pctx->conn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[index],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       pctx->trans = smtp_client_transaction_create(pctx->conn,
+               SMTP_ADDRESS_LITERAL("sender", "example.com"), NULL,
+               test_client_unexpected_reply_finished, pctx);
+       smtp_client_connection_connect(pctx->conn,
+               test_client_unexpected_reply_login_cb, (void *)pctx);
+       smtp_client_transaction_start(pctx->trans,
+               test_client_unexpected_reply_mail_from_cb, pctx);
+       smtp_client_connection_unref(&pctx->conn);
+
+       pctx->to = timeout_add_short(500,
+               test_client_unexpected_reply_submit1, pctx);
+}
+
+static bool
+test_client_unexpected_reply(
+       const struct smtp_client_settings *client_set)
+{
+       struct _unexpected_reply *ctx;
+       unsigned int i;
+
+       ctx = i_new(struct _unexpected_reply, 1);
+       ctx->count = 6;
+
+       smtp_client = smtp_client_init(client_set);
+
+       for (i = 0; i < ctx->count; i++)
+               test_client_unexpected_reply_submit(ctx, i);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_unexpected_reply(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("unexpected reply");
+       test_run_client_server(&smtp_client_set,
+               test_client_unexpected_reply,
+               test_server_unexpected_reply, 6, NULL);
+       test_end();
+}
+
+/*
+ * Partial reply
+ */
+
+/* server */
+
+static int
+test_partial_reply_input_line(struct server_connection *conn,
+       const char *line ATTR_UNUSED)
+{
+       if (conn->state == SERVER_CONNECTION_STATE_EHLO)
+               return 0;
+       o_stream_send_str(conn->conn.output,
+               "500 Command not");
+       server_connection_deinit(&conn);
+       return -1;
+}
+
+static void test_server_partial_reply(unsigned int index)
+{
+       test_server_input_line = test_partial_reply_input_line;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _partial_reply {
+       unsigned int count;
+};
+
+static void
+test_client_partial_reply_reply(const struct smtp_reply *reply,
+       struct _partial_reply *ctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_partial_reply(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _partial_reply *ctx;
+
+       ctx = i_new(struct _partial_reply, 1);
+       ctx->count = 2;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_partial_reply_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_partial_reply_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_partial_reply(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("partial reply");
+       test_run_client_server(&smtp_client_set,
+               test_client_partial_reply,
+               test_server_partial_reply, 1, NULL);
+       test_end();
+}
+
+/*
+ * Bad reply
+ */
+
+/* server */
+
+static int
+test_bad_reply_input_line(struct server_connection *conn,
+       const char *line ATTR_UNUSED)
+{
+       if (conn->state == SERVER_CONNECTION_STATE_EHLO)
+               return 0;
+       o_stream_send_str(conn->conn.output,
+               "666 Really bad reply\r\n");
+       server_connection_deinit(&conn);
+       return -1;
+}
+
+static void test_server_bad_reply(unsigned int index)
+{
+       test_server_input_line = test_bad_reply_input_line;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _bad_reply {
+       unsigned int count;
+};
+
+static void
+test_client_bad_reply_reply(const struct smtp_reply *reply,
+       struct _bad_reply *ctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_bad_reply(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _bad_reply *ctx;
+
+       ctx = i_new(struct _bad_reply, 1);
+       ctx->count = 2;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_bad_reply_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_bad_reply_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_bad_reply(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("bad reply");
+       test_run_client_server(&smtp_client_set,
+               test_client_bad_reply,
+               test_server_bad_reply, 1, NULL);
+       test_end();
+}
+
+/*
+ * Bad greeting
+ */
+
+/* server */
+
+static int
+test_bad_greeting_init(struct server_connection *conn)
+{
+       switch (server_index) {
+       case 0:
+               o_stream_send_str(conn->conn.output,
+                       "666 Mouhahahaha!!\r\n");
+               break;
+       case 1:
+               o_stream_send_str(conn->conn.output,
+                       "446 Not right now, sorry.\r\n");
+               break;
+       case 2:
+               o_stream_send_str(conn->conn.output,
+                       "233 Gimme all your mail, NOW!!\r\n");
+               break;
+       }
+       server_connection_deinit(&conn);
+       return -1;
+}      
+
+static void test_server_bad_greeting(unsigned int index)
+{
+       test_server_init = test_bad_greeting_init;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _bad_greeting {
+       unsigned int count;
+};
+
+struct _bad_greeting_peer {
+       struct _bad_greeting *context;
+       unsigned int index;
+};
+
+static void
+test_client_bad_greeting_reply(const struct smtp_reply *reply,
+       struct _bad_greeting_peer *pctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       switch (pctx->index) {
+       case 0:
+               test_assert(reply->status ==
+                       SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+               break;
+       case 1:
+               test_assert(reply->status == 446);
+               break;
+       case 2:
+               test_assert(reply->status ==
+                       SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+               break;
+       }
+
+       if (--pctx->context->count == 0) {
+               i_free(pctx->context);
+               io_loop_stop(ioloop);
+       }
+       i_free(pctx);
+}
+
+static void
+test_client_bad_greeting_submit(struct _bad_greeting *ctx,
+       unsigned int index)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _bad_greeting_peer *pctx;
+
+       pctx = i_new(struct _bad_greeting_peer, 1);
+       pctx->context = ctx;
+       pctx->index = index;
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[index],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_bad_greeting_reply, pctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+}
+
+static bool
+test_client_bad_greeting(
+       const struct smtp_client_settings *client_set)
+{
+       struct _bad_greeting *ctx;
+
+       ctx = i_new(struct _bad_greeting, 1);
+       ctx->count = 3;
+
+       smtp_client = smtp_client_init(client_set);
+
+       test_client_bad_greeting_submit(ctx, 0);
+       test_client_bad_greeting_submit(ctx, 1);
+       test_client_bad_greeting_submit(ctx, 2);
+       return TRUE;
+}
+
+/* test */
+
+static void test_bad_greeting(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("bad greeting");
+       test_run_client_server(&smtp_client_set,
+               test_client_bad_greeting,
+               test_server_bad_greeting, 3, NULL);
+       test_end();
+}
+
+/*
+ * Command timeout
+ */
+
+/* server */
+
+static int
+test_command_timed_out_input_line(struct server_connection *conn,
+       const char *line ATTR_UNUSED)
+{
+       if (conn->state == SERVER_CONNECTION_STATE_EHLO)
+               return 0;
+       sleep(10);
+       server_connection_deinit(&conn);
+       return -1;
+}
+
+static void test_server_command_timed_out(unsigned int index)
+{
+       test_server_input_line = test_command_timed_out_input_line;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _command_timed_out {
+       unsigned int count;
+};
+
+static void
+test_client_command_timed_out_reply(const struct smtp_reply *reply,
+       struct _command_timed_out *ctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_TIMED_OUT);
+
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_command_timed_out(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _command_timed_out *ctx;
+
+       test_expect_errors(1);
+
+       ctx = i_new(struct _command_timed_out, 1);
+       ctx->count = 1;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_command_timed_out_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_command_timed_out(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+       smtp_client_set.command_timeout_msecs = 1000;
+
+       test_begin("command timed out");
+       test_run_client_server(&smtp_client_set,
+               test_client_command_timed_out,
+               test_server_command_timed_out, 1, NULL);
+       test_end();
+}
+
+/*
+ * Command aborted early
+ */
+
+/* server */
+
+static int
+test_command_aborted_early_input_line(struct server_connection *conn,
+       const char *line ATTR_UNUSED)
+{
+       if (conn->state == SERVER_CONNECTION_STATE_EHLO)
+               return 0;
+
+       sleep(1);
+       o_stream_nsend_str(conn->conn.output, "200 OK\r\n");
+       server_connection_deinit(&conn);
+       return -1;
+}
+
+static void test_server_command_aborted_early(unsigned int index)
+{
+       test_server_input_line = test_command_aborted_early_input_line;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _command_aborted_early {
+       struct smtp_client_command *cmd;
+       struct timeout *to;
+};
+
+static void
+test_client_command_aborted_early_reply(const struct smtp_reply *reply,
+       struct _command_aborted_early *ctx ATTR_UNUSED)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       /* abort does not trigger callback */
+       test_assert(FALSE);
+}
+
+static void
+test_client_command_aborted_early_timeout(
+       struct _command_aborted_early *ctx)
+{
+       timeout_remove(&ctx->to);
+
+       if (ctx->cmd != NULL) {
+               if (debug)
+                       i_debug("ABORT");
+
+               /* abort early */
+               smtp_client_command_abort(&ctx->cmd);
+
+               /* wait a little for server to actually respond to an
+                  already aborted request */
+               ctx->to = timeout_add_short(1000,
+                       test_client_command_aborted_early_timeout, ctx);
+       } else {
+               if (debug)
+                       i_debug("FINISHED");
+
+               /* all done */
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_command_aborted_early(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct _command_aborted_early *ctx;
+
+       ctx = i_new(struct _command_aborted_early, 1);
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       ctx->cmd = smtp_client_command_new(sconn, 0,
+               test_client_command_aborted_early_reply, ctx);
+       smtp_client_command_write(ctx->cmd, "FROP");
+       smtp_client_command_submit(ctx->cmd);
+
+       ctx->to = timeout_add_short(500,
+               test_client_command_aborted_early_timeout, ctx);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_command_aborted_early(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("command aborted early");
+       test_run_client_server(&smtp_client_set,
+               test_client_command_aborted_early,
+               test_server_command_aborted_early, 1, NULL);
+       test_end();
+}
+
+/*
+ * Client deinit early
+ */
+
+/* server */
+
+static int
+test_client_deinit_early_input_line(struct server_connection *conn,
+       const char *line ATTR_UNUSED)
+{
+       if (conn->state == SERVER_CONNECTION_STATE_EHLO)
+               return 0;
+
+       sleep(1);
+       o_stream_nsend_str(conn->conn.output, "200 OK\r\n");
+       server_connection_deinit(&conn);
+       return -1;
+}
+
+static void test_server_client_deinit_early(unsigned int index)
+{
+       test_server_input_line = test_client_deinit_early_input_line;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _client_deinit_early {
+       struct smtp_client_command *cmd;
+       struct timeout *to;
+};
+
+static void
+test_client_client_deinit_early_reply(const struct smtp_reply *reply,
+       struct _client_deinit_early *ctx ATTR_UNUSED)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       /* abort does not trigger callback */
+       test_assert(FALSE); 
+}
+
+static void
+test_client_client_deinit_early_timeout(
+       struct _client_deinit_early *ctx)
+{
+       timeout_remove(&ctx->to);
+
+       /* deinit early */
+       smtp_client_deinit(&smtp_client);
+
+       /* all done */
+       i_free(ctx);
+       io_loop_stop(ioloop);
+}
+
+static bool
+test_client_client_deinit_early(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct _client_deinit_early *ctx;
+
+       ctx = i_new(struct _client_deinit_early, 1);
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+               SMTP_CLIENT_SSL_MODE_NONE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       ctx->cmd = smtp_client_command_new(sconn, 0,
+               test_client_client_deinit_early_reply, ctx);
+       smtp_client_command_write(ctx->cmd, "FROP");
+       smtp_client_command_submit(ctx->cmd);
+
+       ctx->to = timeout_add_short(500,
+               test_client_client_deinit_early_timeout, ctx);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_client_deinit_early(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+
+       test_begin("client deinit early");
+       test_run_client_server(&smtp_client_set,
+               test_client_client_deinit_early,
+               test_server_client_deinit_early, 1, NULL);
+       test_end();
+}
+
+/*
+ * DNS service failure
+ */
+
+/* client */
+
+struct _dns_service_failure {
+       unsigned int count;
+};
+
+static void
+test_client_dns_service_failure_reply(const struct smtp_reply *reply,
+       struct _dns_service_failure *ctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED);
+
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_dns_service_failure(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _dns_service_failure *ctx;
+
+       test_expect_errors(2);
+
+       ctx = i_new(struct _dns_service_failure, 1);
+       ctx->count = 2;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "host.in-addr.arpa", 465,
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_dns_service_failure_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "host.in-addr.arpa", 465,
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_dns_service_failure_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_dns_service_failure(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+       smtp_client_set.dns_client_socket_path = "./frop";
+
+       test_begin("dns service failure");
+       test_run_client_server(&smtp_client_set,
+               test_client_dns_service_failure,
+               NULL, 0, NULL);
+       test_end();
+}
+
+/*
+ * DNS timeout
+ */
+
+/* dns */
+
+static void
+test_dns_timeout_input(struct server_connection *conn ATTR_UNUSED)
+{
+       /* hang */
+       sleep(100);
+       server_connection_deinit(&conn);
+}
+
+static void test_dns_dns_timeout(void)
+{
+       test_server_input = test_dns_timeout_input;
+       test_server_run(0);
+}
+
+/* client */
+
+struct _dns_timeout {
+       unsigned int count;
+};
+
+static void
+test_client_dns_timeout_reply(const struct smtp_reply *reply,
+       struct _dns_timeout *ctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED);
+
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_dns_timeout(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _dns_timeout *ctx;
+
+       test_expect_errors(2);
+
+       ctx = i_new(struct _dns_timeout, 1);
+       ctx->count = 2;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "example.com", 465,
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_dns_timeout_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "example.com", 465,
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_dns_timeout_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_dns_timeout(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+       smtp_client_set.connect_timeout_msecs = 2000;
+       smtp_client_set.dns_client_socket_path = "./dns-test";
+
+       test_begin("dns timeout");
+       test_run_client_server(&smtp_client_set,
+               test_client_dns_timeout, NULL, 0,
+               test_dns_dns_timeout);
+       test_end();
+}
+
+/*
+ * DNS lookup failure
+ */
+
+/* dns */
+
+static void
+test_dns_lookup_failure_input(struct server_connection *conn)
+{
+       o_stream_nsend_str(conn->conn.output,
+               t_strdup_printf("%d\n", EAI_FAIL));
+       server_connection_deinit(&conn);
+}
+
+static void test_dns_dns_lookup_failure(void)
+{
+       test_server_input = test_dns_lookup_failure_input;
+       test_server_run(0);
+}
+
+/* client */
+
+struct _dns_lookup_failure {
+       unsigned int count;
+};
+
+static void
+test_client_dns_lookup_failure_reply(const struct smtp_reply *reply,
+       struct _dns_lookup_failure *ctx)
+{
+       if (debug)
+               i_debug("REPLY: %s", smtp_reply_log(reply));
+
+       test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED);
+
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+}
+
+static bool
+test_client_dns_lookup_failure(
+       const struct smtp_client_settings *client_set)
+{
+       struct smtp_client_connection *sconn;
+       struct smtp_client_command *scmd;
+       struct _dns_lookup_failure *ctx;
+
+       test_expect_errors(2);
+
+       ctx = i_new(struct _dns_lookup_failure, 1);
+       ctx->count = 2;
+
+       smtp_client = smtp_client_init(client_set);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "example.com", 465,
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_dns_lookup_failure_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       sconn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, "example.com", 465,
+               SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+       smtp_client_connection_connect(sconn, NULL, NULL);
+       scmd = smtp_client_command_new(sconn, 0,
+               test_client_dns_lookup_failure_reply, ctx);
+       smtp_client_command_write(scmd, "FROP");
+       smtp_client_command_submit(scmd);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_dns_lookup_failure(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+       smtp_client_set.dns_client_socket_path = "./dns-test";
+
+       test_begin("dns lookup failure");
+       test_run_client_server(&smtp_client_set,
+               test_client_dns_lookup_failure, NULL, 0,
+               test_dns_dns_lookup_failure);
+       test_end();
+}
+
+/*
+ * Authentication failed
+ */
+
+/* server */
+
+static int
+test_authentication_failed_input_line(struct server_connection *conn,
+       const char *line ATTR_UNUSED)
+{
+       switch (conn->state) {
+       case SERVER_CONNECTION_STATE_EHLO:
+               if (server_index == 1) {
+                       o_stream_send_str(conn->conn.output,
+                               "250-testserver\r\n"
+                               "250-PIPELINING\r\n"
+                               "250-ENHANCEDSTATUSCODES\r\n"
+                               "250-AUTH PLAIN\r\n"
+                               "250 DSN\r\n");
+                       conn->state = SERVER_CONNECTION_STATE_MAIL_FROM;
+                       return 1;
+               }
+               break;
+       case SERVER_CONNECTION_STATE_MAIL_FROM:
+               if (server_index == 1) {
+                       o_stream_send_str(conn->conn.output,
+                               "535 5.7.8 "
+                               "Authentication credentials invalid\r\n");
+                       sleep(10);
+                       server_connection_deinit(&conn);
+                       return -1;
+               }
+               break;
+       default:
+               break;
+       }
+       return 0;
+}
+
+static void test_server_authentication_failed(unsigned int index)
+{
+       test_server_input_line = test_authentication_failed_input_line;
+       test_server_run(index);
+}
+
+/* client */
+
+struct _authentication_failed {
+       unsigned int count;
+};
+
+struct _authentication_failed_peer {
+       struct _authentication_failed *context;
+       unsigned int index;
+
+       struct smtp_client_connection *conn;
+       struct smtp_client_transaction *trans;
+};
+
+static void
+test_client_authentication_failed_login_cb(const struct smtp_reply *reply,
+       void *context)
+{
+       struct _authentication_failed_peer *pctx =
+               (struct _authentication_failed_peer *)context;
+
+       if (debug) {
+               i_debug("LOGIN REPLY[%u]: %s",
+                       pctx->index, smtp_reply_log(reply));
+       }
+
+       switch (pctx->index) {
+       case 0:
+               test_assert(reply->status ==
+                       SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED);
+               break;
+       case 1:
+               test_assert(reply->status == 535);
+               break;
+       }
+}
+
+static void
+test_client_authentication_failed_mail_from_cb(const struct smtp_reply *reply,
+       struct _authentication_failed_peer *pctx)
+{
+       if (debug) {
+               i_debug("MAIL FROM REPLY[%u]: %s",
+                       pctx->index, smtp_reply_log(reply));
+       }
+
+       switch (pctx->index) {
+       case 0:
+               test_assert(reply->status ==
+                       SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED);
+               break;
+       case 1:
+               test_assert(reply->status == 535);
+               break;
+       }
+}
+
+static void
+test_client_authentication_failed_rcpt_to_cb(const struct smtp_reply *reply,
+       struct _authentication_failed_peer *pctx)
+{
+       if (debug) {
+               i_debug("RCPT TO REPLY[%u]: %s",
+                       pctx->index, smtp_reply_log(reply));
+       }
+
+       switch (pctx->index) {
+       case 0:
+               test_assert(reply->status ==
+                       SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED);
+               break;
+       case 1:
+               test_assert(reply->status == 535);
+               break;
+       }
+}
+
+static void
+test_client_authentication_failed_rcpt_data_cb(const struct smtp_reply *reply,
+       struct _authentication_failed_peer *pctx)
+{
+       if (debug) {
+               i_debug("RCPT DATA REPLY[%u]: %s",
+                       pctx->index, smtp_reply_log(reply));
+       }
+
+       test_assert(FALSE);
+}
+
+static void
+test_client_authentication_failed_data_cb(const struct smtp_reply *reply,
+       struct _authentication_failed_peer *pctx)
+{
+       if (debug) {
+               i_debug("DATA REPLY[%u]: %s",
+                       pctx->index, smtp_reply_log(reply));
+       }
+
+       switch (pctx->index) {
+       case 0:
+               test_assert(reply->status ==
+                       SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED);
+               break;
+       case 1:
+               test_assert(reply->status == 535);
+               break;
+       }
+}
+
+static void
+test_client_authentication_failed_finished(struct _authentication_failed_peer *pctx)
+{
+       struct _authentication_failed *ctx = pctx->context;
+
+       if (debug)
+               i_debug("FINISHED[%u]", pctx->index);
+       if (--ctx->count == 0) {
+               i_free(ctx);
+               io_loop_stop(ioloop);
+       }
+
+       pctx->trans = NULL;
+       i_free(pctx);
+}
+
+static void
+test_client_authentication_failed_submit(struct _authentication_failed *ctx,
+       unsigned int index)
+{
+       struct _authentication_failed_peer *pctx;
+       struct smtp_client_settings smtp_set;
+       static const char *message =
+               "From: stephan@example.com\r\n"
+               "To: timo@example.com\r\n"
+               "Subject: Frop!\r\n"
+               "\r\n"
+               "Frop!\r\n";
+       struct istream *input;
+
+       pctx = i_new(struct _authentication_failed_peer, 1);
+       pctx->context = ctx;
+       pctx->index = index;
+
+       i_zero(&smtp_set);
+       smtp_set.username = "peter.wolfsen";
+       smtp_set.password = "crybaby";
+
+       pctx->conn = smtp_client_connection_create(smtp_client,
+               SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[index],
+               SMTP_CLIENT_SSL_MODE_NONE, &smtp_set);
+       pctx->trans = smtp_client_transaction_create(pctx->conn,
+               SMTP_ADDRESS_LITERAL("sender", "example.com"), NULL,
+               test_client_authentication_failed_finished, pctx);
+       smtp_client_connection_connect(pctx->conn,
+               test_client_authentication_failed_login_cb, (void *)pctx);
+       smtp_client_transaction_start(pctx->trans,
+               test_client_authentication_failed_mail_from_cb, pctx);
+       smtp_client_connection_unref(&pctx->conn);
+
+       smtp_client_transaction_add_rcpt(pctx->trans,
+               SMTP_ADDRESS_LITERAL("rcpt", "example.com"), NULL,
+               test_client_authentication_failed_rcpt_to_cb,
+               test_client_authentication_failed_rcpt_data_cb, pctx);
+
+       input = i_stream_create_from_data(message, strlen(message));
+       i_stream_set_name(input, "message");
+
+       smtp_client_transaction_send(pctx->trans,
+               input, test_client_authentication_failed_data_cb, pctx);
+       i_stream_unref(&input);
+}
+
+static bool
+test_client_authentication_failed(
+       const struct smtp_client_settings *client_set)
+{
+       struct _authentication_failed *ctx;
+       unsigned int i;
+
+       ctx = i_new(struct _authentication_failed, 1);
+       ctx->count = 2;
+
+       smtp_client = smtp_client_init(client_set);
+
+       for (i = 0; i < ctx->count; i++)
+               test_client_authentication_failed_submit(ctx, i);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_authentication_failed(void)
+{
+       struct smtp_client_settings smtp_client_set;
+
+       test_client_defaults(&smtp_client_set);
+
+       test_begin("authentication failed");
+       test_expect_errors(1);
+       test_run_client_server(&smtp_client_set,
+               test_client_authentication_failed,
+               test_server_authentication_failed, 2, NULL);
+       test_end();
+}
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+       test_unconfigured_ssl,
+       test_unconfigured_ssl_abort,
+       test_host_lookup_failed,
+       test_connection_refused,
+       test_connection_lost_prematurely,
+       test_connection_timed_out,
+       test_broken_payload,
+       test_connection_lost,
+       test_unexpected_reply,
+       test_partial_reply,
+       test_bad_reply,
+       test_bad_greeting,
+       test_command_timed_out,
+       test_command_aborted_early,
+       test_client_deinit_early,
+       test_dns_service_failure,
+       test_dns_timeout,
+       test_dns_lookup_failure,
+       test_authentication_failed,
+       NULL
+};
+
+/*
+ * Test client
+ */
+
+static void
+test_client_defaults(struct smtp_client_settings *smtp_set)
+{
+       /* client settings */
+       i_zero(smtp_set);
+       smtp_set->my_hostname = "frop.example.com";
+       smtp_set->debug = debug;
+}
+
+static void test_client_deinit(void)
+{
+       if (smtp_client != NULL)
+               smtp_client_deinit(&smtp_client);
+       smtp_client = NULL;
+}
+
+/*
+ * Test server
+ */
+
+/* client connection */
+
+static void
+server_connection_input(struct connection *_conn)
+{
+       struct server_connection *conn = (struct server_connection *)_conn;
+       const char *line;
+
+       if (test_server_input != NULL) {
+               test_server_input(conn);
+               return;
+       }
+
+       for (;;) {
+               if (conn->state == SERVER_CONNECTION_STATE_FINISH) {
+                       const unsigned char *data;
+                       size_t size;
+                       int ret;
+
+                       if (conn->dot_input == NULL)
+                               conn->dot_input = i_stream_create_dot(conn->conn.input, TRUE);
+                       while ((ret=i_stream_read_more(conn->dot_input,
+                               &data, &size)) > 0) {
+                               if (test_server_input_data != NULL) {
+                                       if (test_server_input_data(conn, data, size) < 0)
+                                               return;
+                               }
+                               i_stream_skip(conn->dot_input, size);
+                       }
+
+                       if (ret == 0)
+                               return;
+                       if (conn->dot_input->stream_errno != 0) {
+                               i_error("Failed to read message payload: %s",
+                                       i_stream_get_error(conn->dot_input));
+                               server_connection_deinit(&conn);
+                               return;
+                       }
+
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.0.0 Ok: queued as 73BDE342129\r\n");
+                       conn->state = SERVER_CONNECTION_STATE_MAIL_FROM;
+                       continue;
+               }
+
+               line = i_stream_read_next_line(conn->conn.input);
+               if (line == NULL) {
+                       if (conn->conn.input->eof ||
+                               conn->conn.input->stream_errno != 0)
+                               server_connection_deinit(&conn);
+                       return;
+               }
+
+               if (test_server_input_line != NULL) {
+                       if (test_server_input_line(conn, line) != 0)
+                               return;
+               }
+
+               switch (conn->state) {
+               case SERVER_CONNECTION_STATE_EHLO:
+                       o_stream_send_str(conn->conn.output,
+                               "250-testserver\r\n"
+                               "250-PIPELINING\r\n"
+                               "250-ENHANCEDSTATUSCODES\r\n"
+                               "250 DSN\r\n");
+                       conn->state = SERVER_CONNECTION_STATE_MAIL_FROM;
+                       return;
+               case SERVER_CONNECTION_STATE_MAIL_FROM:
+                       if (strncmp(line, "AUTH ", 5) == 0) {
+                               o_stream_send_str(conn->conn.output,
+                                       "235 2.7.0 "
+                                       "Authentication successful\r\n");
+                               continue;
+                       }
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.0 Ok\r\n");
+                       conn->state = SERVER_CONNECTION_STATE_RCPT_TO;
+                       continue;
+               case SERVER_CONNECTION_STATE_RCPT_TO:
+                       o_stream_send_str(conn->conn.output,
+                               "250 2.1.5 Ok\r\n");
+                       conn->state = SERVER_CONNECTION_STATE_DATA;
+                       continue;
+               case SERVER_CONNECTION_STATE_DATA:
+                       o_stream_send_str(conn->conn.output,
+                               "354 End data with <CR><LF>.<CR><LF>\r\n");
+                       conn->state = SERVER_CONNECTION_STATE_FINISH;
+                       continue;
+               case SERVER_CONNECTION_STATE_FINISH:
+                       break;
+               }
+               i_unreached();
+       }
+}
+
+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) {
+               if (test_server_init(conn) != 0)
+                       return;
+       }
+
+       if (test_server_input == NULL) {
+               o_stream_send_str(conn->conn.output,
+                       "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+       }
+}
+
+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);
+
+       i_stream_unref(&conn->dot_input);
+
+       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;
+
+       if (dns_pid != (pid_t)-1) {
+               (void)kill(dns_pid, SIGKILL);
+               (void)waitpid(dns_pid, NULL, 0);
+               dns_pid = (pid_t)-1;
+       }
+}
+
+static void test_run_client_server(
+       const struct smtp_client_settings *client_set,
+       test_client_init_t client_test,
+       test_server_init_t server_test,
+       unsigned int server_tests_count,
+       test_dns_init_t dns_test)
+{
+       unsigned int i;
+
+       server_pids = NULL;
+       server_pids_count = 0;
+
+       if (server_tests_count > 0) {
+               int fds[server_tests_count];
+
+               bind_ports = i_new(in_port_t, server_tests_count);
+
+               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];
+                       if ((server_pids[i] = fork()) == (pid_t)-1)
+                               i_fatal("fork() failed: %m");
+                       if (server_pids[i] == 0) {
+                               lib_signals_ignore(SIGPIPE, TRUE);
+                               server_pids[i] = (pid_t)-1;
+                               server_pids_count = 0;
+                               hostpid_init();
+                               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);
+                               i_close_fd(&fd_listen);
+                               i_free(bind_ports);
+                               i_free(server_pids);
+                               /* wait for it to be killed; this way, valgrind will not
+                                  object to this process going away inelegantly. */
+                               sleep(60);
+                               exit(1);
+                       }
+                       i_close_fd(&fd_listen);
+               }
+               if (debug)
+                       i_debug("client: PID=%s", my_pid);
+       }
+
+       if (dns_test != NULL) {
+               int fd;
+
+               i_unlink_if_exists("./dns-test");
+               fd = net_listen_unix("./dns-test", 128);
+               if (fd == -1) {
+                       i_fatal("listen(./dns-test) failed: %m");
+               }
+
+               fd_listen = fd;
+               if ((dns_pid = fork()) == (pid_t)-1)
+                       i_fatal("fork() failed: %m");
+               if (dns_pid == 0) {
+                       lib_signals_ignore(SIGPIPE, TRUE);
+                       dns_pid = (pid_t)-1;
+                       hostpid_init();
+                       if (debug)
+                               i_debug("dns server: PID=%s", my_pid);
+                       /* child: server */
+                       ioloop = io_loop_create();
+                       dns_test();
+                       io_loop_destroy(&ioloop);
+                       i_close_fd(&fd_listen);
+                       /* wait for it to be killed; this way, valgrind will not
+                          object to this process going away inelegantly. */
+                       sleep(60);
+                       exit(1);
+               }
+               i_close_fd(&fd_listen);
+       }
+
+       /* parent: client */
+
+       usleep(100000); /* wait a little for server setup */
+
+       lib_signals_ignore(SIGPIPE, TRUE);
+       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);
+
+       i_unlink_if_exists("./dns-test");
+}
+
+/*
+ * 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);
+
+       while ((c = getopt(argc, argv, "D")) > 0) {
+               switch (c) {
+               case 'D':
+                       debug = TRUE;
+                       break;
+               default:
+                       i_fatal("Usage: %s [-D]", argv[0]);
+               }
+       }
+
+       /* 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);
+}